안녕하세요. 산드로 만쿠소 입니다. 이 스크린 케스트에선 테스트 코드가 없는 기존 코드에 먼저 테스트를 작성하고요 테스트 커버리지가 100%가 된 후에 리팩토링으로 개선하는 것을 보여드리겠습니다. 예제의 비즈니스 요구사항은 여행자를 위한 소셜 네트워킹 서비스 같은 것입니다. 콘텐츠를 보려면 로그인을 해야 하고 로그인 후에 다른 사람의 여행 일정을 보려면 그 사람과 친구인 경우에만 조회가 가능합니다. 페이스 북 처럼 말입니다. 그 사람과 친구라면 그 사람의 여행 목록을 볼 수 있고 친구가 아니라면 볼 수가 없습니다. 이 카타에는 규칙이 몇 개 있습니다. 테스트가 없는 코드는 수정을 할 수 없습니다. 따라서 기존 제품 코드를 수정하려면 먼저 기존 코드에 테스트 코드를 작성해야 합니다. 유일한 예외는 IDE의 리팩토링 기능을 사용하는 것 입니다. 절대 소스 코드에 타이핑해서 수정을 하면 안됩니다. 테스트를 작성해가는 순서가 있는데요 소스 코드가 이렇게 생겼다면요 여기 인덴트가 깊은 부분 말고 뎁스가 가장 얕은 부분부터 테스트를 작성할 것 입니다. 그 후에 그 다음으로 얕은 부분의 테스트를 작성하는 거지요. 이런 순서로 테스트를 작성하면 테스트를 작성함에 따라 제품 코드를 더 깊게 이해해 갈 수 있고 테스트 스윗도 점진적으로 만들어 갈 수 있습니다. 만약에 반대로 가장 깊은 부분부터 테스트를 작성하려면 처음부터 대상 코드를 모두 이해해야 하고 많은 양의 테스트 데이터를 준비해야 합니다. 그래서 코드의 얕은 부분에서 깊은 부분으로 테스트하는 것이 좋습니다. 이제 코드를 볼까요 TripService 클래스인데요 서버 단 코드겠지요 기본적으로 유저의 여행 목록을 조회합니다. 파라메터로 유저를 받고 세션 정보에서 로그인 유저 정보를 조회하고 로그인하지 않았다면 예외를 던집니다. 로그인 했다면 로그인 유저가 파라메터로 전달된 유저와 친구인지 확인하고 서로 친구라면 전달된 유저의 여행 목록을 조회합니다. 다 파악한 것은 아닙니다만 일단 테스트를 작성해보겠습니다. 코딩 하기에 편하게 에디터 화면을 배치하도록 하겠습니다. 저는 보통 화면을 둘로 나눠서 하나는 제품 코드를 두고 다른 하나는 테스트 코드를 띄웁니다. 화면을 계속 스위칭할 필요가 없어서 아주 편리해요. 맨 처음으로 할 것은 가장 얕은 부분을 찾는 겁니다. 그게 어떤 코드인지는 잊어도 되요 적어도 잠시 동안은요 레거시 코드에 작업을 할 때 맨 처음으로 할 것은요 레거시 코드가 뭘 하는지 파악하려면요 가장 얕은 부분을 찾는 것 입니다. Logged user 변수가 Null이 아니라면 예외를 던지네요. 유저가 로그인 상태가 아니면 예외를 던져요. 테스트 코드로 실제로 그렇게 동작하는지 봅시다. 유저가 로그인 상태가 아니면 예외를 던진다 예외를 적어줍시다 UserNotLoggedInException이 나와야 하는 거지요 TripService 인스턴스를 만들어 봅시다 먼저 Import 하고 그 다음에 여행 서비스에서 유저의 여행 목록을 조회하고 일단 유저에는 null을 넣고요 지금 인피니테스트를 사용 중인데요 여기 빨간색으로 결과가 표시되지요 제가 코드를 저장할 때마다 자동으로 필요한 테스트를 실행합니다. 매번 손으로 돌릴 필요가 없어요. 그런데 원래는 이게.. 녹색이어야 하는데요 뭔가 잘못된 거에요 그래서 테스트를 직접 돌리면 어떻게 되어야하냐면.. UserNotLoggedInException이 나와야 해요 UserNotLoggedInException이 나왔어야 하는데 의존 클래스 관련된 다른 종류의 예외가 생겼군요 어떻게 된거냐하면요 이 부분이 실행되었을 때에요 유저 세션의 로그인 유저를 조회하는데요 유저 세션 코드를 까서 보면 물론 이 건 연습을 위한 예제 코드라서 그래요 예외가 생기게 일부러 작성해놨습니다. 왜냐하면 단위테스트는 다른 클래스들을 호출하면 안되니까요 실수로 이 걸 실행시키면 바로 알 수 있게 하려고요. 만약에 실제 Http Session을 사용하고 있다면 실제 데이터 베이스를 조회하려고 할지도 몰라요 단위테스트하는데 그러면 안되지요. 이제 제대로 테스트하려면 이 코드를 사용하지 않게 만들어야 합니다. 문제는 이게 싱글톤이라 Mock으로 대체할 수가 없다는 점 입니다. TripService 클래스를 손으로 수정하는 건 금지되어있으니 다른 방법을 찾아야겠지요. UserSession을 Mock으로 대체할 방법을 찾아야 합니다. 처음에 말한 대로 코드를 수정하는 유일한 방법은 IDE의 리팩토링 기능을 이용하는 것 입니다. 코드에 직접 타이핑하는 것은 안됩니다. 그래서 연결점(Seam)을 만들려고 합니다. 연결점(Seam)은 클래스들이 이어지는 지점을 말합니다. 서로 분리되는 지점이기도 하지요. 이제 TripService와 UserSession 사이에 연결점을 만들겠습니다. 메소드를 하나 만드는데요 이름은 getLoggedInUser로 하고 접근제한자 protected로 이렇게 TripService가 다른 클래스에 접근하는 부분을 분리해서 연결점을 만들었습니다. 나중에 또 손보겠지만요 이제 TripService를 상속받는 TestableTripService 클래스를 만들 수 있겠지요 여기에서 getLoggedInUser 메소드를 오버라이드해서 null을 반환하게 만듭니다. 이제 테스트는 TripService 대신에 TestableTripService를 사용하는 겁니다. 테스트를 실행하면 보시는 데로 테스트가 통과합니다. 가끔 인피니테스트는 이상하게 동작할 때가 있어서 믿기 어려운 경우도 있습니다. 지금도 실패했다고 빨갛게 표시되었지만 실제로는 모두 정상이었지요. 이걸로도 충분하겠지만 테스트를 다시 돌리면.. 모두 통과합니다. 코드 커버리지를 확인하면 제일 얕은 부분의 코드가 실행되었는지 확인할 수 있습니다. 이 것도 팁이에요. 저는 레거시 코드로 작업을 할 때 항상 코드 커버리지를 확인합니다. 보통 코드 커버리지라고 하면 테스트 커버리지라고 생각하는 경우가 많은데요 저는 커버리지 숫자는 신경 쓰지 않습니다. 레거시 코드에 작업을 할 때에는 테스트하려는 코드가 실제로 실행되었는지가 중요합니다. 긍정 오류나 부정 오류 등을 방지해서 정확하게 작업할 수 있습니다. 좋습니다. 커버리지 표시는 지우고요. 그나저나 테스트 코드가 마땅치 않네요 이 테스트는요 유저가 로그인 상태가 아니면 예외를 던진다 이건 로그인하지 않은 유저에 대한 테스트인데요 로그인하지 않은 유저는 어디를 보면 알 수 있지요? TestableTripService를 생성해서 Null을 인자로 메소드를 호출해서 예외가 생기는데요. 로그인 하지 않은 유저는 어디에 있을까요? 여기지요. 감춰져 있습니다. 그럼 유저 상태가 표현되도록 수정해야겠지요. 그나저나 이 부분은 조금 특이하지요? 자바에서는 조금 이상한 방식이지만 루비에서 종종 사용하는 방법입니다. 에디터에서 코드를 이렇게 접으면 이렇게 명세서처럼 표현이 됩니다. 그냥 명세서인 것처럼 읽는 거지요 "trip service should do something" 저는 이런 방식을 좋아합니다. 로그인 유저 정보가 전혀 안보이니 하나 만들도록 하겠습니다. loggedInUser를 null로 만들고 멤버 변수로 만든 후에 로그인 유저를 조회할 때에 loggedInUser 필드를 반환하게 하는 거지요. 조금 더 좋아졌지요? 테스트 코드에 유저가 표현되고 있어요. 음.. Null은 좀 별로네요 여기 이 Null의 의미가 분명하지가 않아요. 유저가 로그인하지 않았다는 건 보통 웹 어플리케이션에서 로그인하지 않은 유저는 게스트지요. 여기에 비즈니스 개념을 적용해 볼 수 있겠네요 비즈니스 인력과 대화할 때 사용하는 표현들이요. 네 도메인 언어를 사용하기 시작했습니다. 이 건 여행 목록을 조회할 유저지요 Null 말고 더 명시적으로 Unused User라고 바꾸겠습니다. 어디 볼까요 이 번에는 인피니테스트가 제대로 동작할까요.. 아니군요. 안타깝게도 인피니테스트를 믿기 어렵겠네요. 테스트 코드가 많이 개선되었어요. 그 사이에 인피니테스트가 정상으로 돌아왔네요. 말씀 드렸던 것처럼 이 건 유저가 로그인하지 않았으면 예외를 던지는 테스트인데요 현재 유저는 게스트 그리고 예외를 던지지요 좋습니다. 이제 코드 커버리지를 다시 돌려보겠습니다. 이제 코드 커버리지를 다시 돌려보겠습니다. 그 전에 JUnit을 실행하고요 그냥 확실하게 하는 거에요 좋습니다. 이제 코드 커버리지를 실행합니다. 자 이제 코드 커버리지를 보면요 다음으로 얕은 부분을 쉽게 찾을 수 있습니다. 이 For 반복문이겠지요 친구인 경우라면.. 여기가 제일 깊은 부분이네요 이 부분이 실행되려면 이 값이 true 이어야 하니까요 그러니까 여기까지 실행되려면.. 유저는 로그인 된 상태인데 조회하려는 유저와는 친구가 아닌 경우입니다. 그럼 이 시나리오를 검증하는 테스트를 작성하겠습니다. 커버리지는 그대로 두고요 테스트 이름은 유저가 서로 친구가 아닌 경우에는 여행 목록을 반환하지 않는다로 하겠습니다. 여기 코드를 가져와서요 이제는 로그인한 유저가 필요하지요 이름은 Registered User로 하고 멤버 변수를 만들겠습니다. 여행 목록이 필요하겠지요 이름은 friendTrips로 하고 import 하고 이제 제대로 된 유저가 필요하겠지요 여기에요 그러니까 이름은 friend로 하고 지금 친구가 필요하지요 친구를 추가해주고 브라질로 여행을 간다고 합시다 이제 추가한 친구를 멤버 변수로 만들겠습니다. 친구를 만들었지요 이 친구는 브라질로 여행을 가는 또 다른 친구가 있습니다. 하지만 현재 로그인한 유저와는 친구가 아니네요. 그래서 반환된 여행 목록은 크기가 0 이어야 합니다. size() 대신에 isEmpty를 사용했어도 무방하겠지요. 인피니테스트는 잘 돌고 있군요 확실하게 JUnit을 직접 실행하겠습니다. 테스트는 통과했습니다. 이미 동작하던 코드들이니 당연히 테스트는 통과하겠지요. 지금은 코드가 어떻게 동작하는지 알아보려고 테스트를 작성하니까요 당연한 겁니다. 코드 커버리지를 다시 돌려봅시다. 커버리지를 보면 조금 더 진도가 나간 것을 알 수 있습니다. 보시는 것처럼 조금 더 깊이 들어갔어요 어떤 코드 였냐면 유저가 로그인 상태이긴 해도 서로 친구는 아닌 경우였지요 이 부분이 항상 false를 반환했지요 아직 끝난 게 아닙니다. 테스트에 중복된 부분들이 있으니 리팩토링을 해야 합니다. 중복 코드를 없애겠습니다. @Before 메소드를 만들고요 로컬 변수를 멤버 변수로 바꾸겠습니다. 좋습니다. 좋아요. 위로 옮기겠습니다. 좋습니다. 이제 제일 깊은 부분을 테스트하겠습니다. 이 부분이 실행되려면 로그인 유저가 조회 대상인 유저와 친구이고 여행이 조회되는 시나리오가 필요합니다. 유저가 서로 친구인 경우에 친구의 여행 목록을 반환한다. 이 걸 테스트 해야 하지요. 이 부분을 옮겨서... 소스 코드는 절대 Copy & Paste 하지 마세요. 저는 절대 그러지 않습니다. 지금 본 것은 착각입니다. 소스 코드는 절대 Copy & Paste 하지 마세요. 더 추가해야 할 것은요 로그인 유저를 친구로 추가해주고 여행도 하나 더 추가합시다 인피니테스트에는 테스트가 실패했다고 나오지만요 유저들끼리 친구가 되었으니 테스트는 통과할 겁니다. 저장을 하면 여전히 빨간색이네요 테스트를 직접 실행하면 이런 단축키를 잘 못 눌렀네요 윈도우즈를 사용했었는데 맥이랑 인텔리제이랑 이클립스랑 바꾸다 보니 헷갈리네요. 그래도 역시 테스트가 실패합니다. 왜냐하면 화면 배치는 일단 돌려놓고 테스트가 여전히 실패하는 이유는 이 TripDao 때문입니다. 이건 Static 메소드이고 메소드를 실행하면 그냥 예외를 던지게 만들어놨습니다. TripDAO를 이렇게 만들어 놓은 이유는요 단위 테스트에서는 이런 DAO 같은 다른 클래스들을 실행시키면 안되는데 혹시 실수로 TripDAO를 실행한 경우 바로 알 수 있게 하려고 입니다. 실제 시스템의 DAO는 데이터베이스에 연결해서 데이터를 조회하겠지만 테스트에서는 그러면 안됩니다. 이제 UserSession에 했던 것과 같은 작업을 할 겁니다. 싱글톤이나 Static 메소드 호출 같은 강한 의존 관계를 떼어내는 거지요 메소드에서 객체를 생성하는 것도 똑 같습니다. 자 그러면 메소드를 추가하겠습니다. 이름은 tripsBy로 하고 접근제한자는 protected 이제 테스트 코드로 돌아가서 똑 같은 작업을 하겠습니다. 기본적으로 여행 목록을 반환할 건데요 조회할 유저를 받아서 이 테스트 유저는 여행 일정을 두 개 가지고 있으니까 그 걸 반환할 겁니다. the user dot trips 바로 테스트 결과에 반영이 되었군요. 코드 커버리지를 확인하면 좋습니다. 이제 기존 코드의 대부분이 테스트 되었네요. 기존 코드를 상속 받은 부분은 테스트가 아니라고 할 수도 있겠지만 작은 연결점이니까 괜찮아요 작은 연결점이니까 괜찮아요 이게 최종 모습이 아니기도 하고요 조금 더 정리를 해 볼까요 보통은요.. 이 건 여기에도 있고 저기에도 있네요 이런 중복 코드는 절대 좋지 않지요 이 건 남겨 두고요 이 건 이쪽으로 옮기겠습니다. 그러면 다른 모든 테스트에 공통이 되지요 로그인 된 유저가 필요한 모든 테스트에 유효합니다. 그런 후에 저장하고 테스트를 직접 돌리면 여전히 녹색이네요 좋습니다. 이 걸로 첫 단계가 완료되었습니다. 테스트가 다 작성되었으니 이제 리팩토링을 시작할 수 있게 되었습니다. 커밋하기 딱 좋은 시점이네요. git status git commit unit tests for trip service 좋습니다. 그런데 이 테스트를 다시 보면요 레거시 코드는 종종.. 지금은 아주 간단한 예제를 보고 있습니다만 훨씬 복잡한 메소드를 생각해보세요 예제에선 데이터 객체를 하나만 만들어도 충분했지만 예제에선 데이터 객체를 하나만 만들어도 충분했지만 코드를 제대로 테스트하려면 종종 거대하고 복잡한 테스트 데이터를 만들어야 하는 경우가 있습니다. 거대하고 복잡한 테스트 데이터를 만들어야 하는 경우가 있습니다. 그럼 테스트 데이터를 생성하는 코드를 정리해볼까요? 여기 저기에 중복 코드들이 있지요 빌더 패턴을 적용해보겠습니다. 빌더 패턴을 적용해보겠습니다. 기본적으로 이런 식으로 사용하게 되는 거지요 테스트용 유저를 생성하는데 ANOTHER_USER, loggedInUser와 친구이고 브라질과 런던으로 여행을 가지요. 이런 식으로 테스트 데이터를 만드는 거에요. 가독성이 훨씬 좋습니다. 그럼 실제로 구현을 해볼까요 일단 build 메소드를 추가합시다. 빌더 패턴을 적용하는 거지요. UserBuilder를 추가하고 이런 모양을 원하는 겁니다. 이 부분은 주석 처리하고요 aUser 메소드를 만들고요 메소드를 이어서 호출 할 수 있도록 계속 같은 오브젝트를 반환합니다 이러면 메소드를 계속 이어서 호출할 수 있게 됩니다. UserBuilder를 생성해서 반환하고 다음 메소드를 만들어 봅시다 역시 UserBuilder를 반환하고요 두 개의 유저를 받고 있습니다. 항상 말 했듯이 프로그래밍에는 딱 3개의 숫자만 존재합니다. 0, 1, 그리고 많이 입니다. 그 밖에는 없어요. 인자를 여러 개를 받도록 변경합시다 필요한 수만큼 입력할 수 있도록 가변길이 인자를 사용하겠습니다. 이름은 friends 로 하고 friends 변수에 할당하고 계속 UserBuilder 인스턴스를 반환합니다. 이 것도 만들어 주고요 배열은 반드시 초기화 해야겠지요 좋습니다. 다음 메소드를 만들어 볼까요 똑 같이 하는 겁니다. 메소드에 원하는 개 수만큼 여행 정보를 넣어 줄 수 있겠지요 멤버 변수로 만들고요 저는 보통 읽기 좋은 모양으로 사용하는 코드를 작성하고 그거에 맞춰서 구현 코드를 생성해갑니다. 이 번엔 build 메소드를 생성합니다 맨 밑으로 옮기는 게 좋겠지요 build 메소드가 실제로 객체를 생성하게 됩니다. 유저를 생성하고 이 유저를 반환하게 하고 그리고 여행 목록을 추가하고 친구들을 추가합니다. 메소드 이름은 파라메터 이름과 함께 정하는 게 좋습니다. 읽기에도 좋고 이름에 중복을 피할 수도 있지요. 여행 정보부터 시작할까요 여행을 추가하고 친구 목록도 똑같이 하겠습니다 메소드를 만들고 좋습니다. 인피니테스트 결과도 녹색이군요. 이제 이건 지우고요 여기에도 UserBuilder를 사용할 수 있겠지요 수정 합시다 좋고요. 이 건 주석 처리하고. 테스트를 다시 실행하면, 여전히 성공합니다. 좋습니다. 테스트 가독성이 조금 더 좋아졌습니다. 이제 빌더 클래스를 별도 파일로 빼겠습니다. Type To New File 기능을 사용해서 별도 클래스로 만들어서 일단 TripServiceTest와 같은 위치에 두겠습니다. 일단 TripServiceTest와 같은 위치에 두겠습니다. 이제 분리되었지요. static import로 변경하고 이 것도 static import로 변경하고 코드 모양을 맞추겠습니다. 좋고요. import를 정리하고요 사용하지 않는 import가 있으면 안되겠지요 좋습니다. 이제 테스트가 훨씬 마음에 드네요 테스트를 읽기가 훨씬 좋아졌네요. 이제 커밋을 해야겠지요? 봅시다 UserBuilder created and used in TripServiceTest 좋습니다. 이제 진짜로 리팩토링을 시작할 수 있겠습니다. 아까 이야기 했던 게요 어디 있더라 테스트는 얕은 부분부터 깊은 부분으로 작성하는데요 하지만 리팩토링은 그 반대로 진행합니다. 가장 깊은 부분에서 시작해서 얕은 부분으로 가는 거지요. 왜냐하면 중간 부분부터 리팩토링을 하려면 코드를 전부 이해하고 있어야 하기 때문입니다. 일반적으로 레거시 코드는 글로벌 변수가 많고 의존관계도 복잡하고 변수도 여기 저기서 세팅하고 사용되는데요. 가장 깊은 부분부터 시작하면요 가장 깊은 부분은 뭔가에 의존하지 않습니다. 다른 곳에서 주는 정보를 사용해서 뭔가 하는 게 일반적입니다. 깊은 부분을 추출하면요 깊은 부분부터 리팩토링을 시작하면 코드가 점점 줄어들게 됩니다. 소스 코드를 볼까요 가장 깊은 부분은요 아까 본 것처럼 여기입니다. 이 코드가 실행되려면 이게 true 여야 하는데 여기에서 세팅되는군요. 이 예제 코드는요 가장 깊은 부분에서 하는 일이 별로 없어요 DAO를 호출하는 게 전부라서 특별히 리팩토링 할게 없습니다. 다음으로 깊은 부분은 여기인데요 네 이 부분이요 이 부분을 수정해보겠습니다. 레거시 코드에 작업을 할 때에 아주 중요한 것은 메소드가 이렇게 큰 원인을 파악하는 겁니다. 왜 이렇게 복잡할까요? 일반적으로 메소드가 하는 일이 너무 많아서입니다. 하는 일이 많으니 크기가 커지겠지요 처리 알고리즘을 개선하거나 메소드를 추출하는 것 보다 더 중요한 것은 정말 이 일을 이 메소드가 하는 게 적절한가를 지속적으로 생각해봐야 합니다. 이 경우에는 이 부분을 보면 유저 객체에 친구 정보를 요청하지요 유저 객체에서 받은 친구 목록을 하나 하나 검사해서 로그인 유저가 포함돼있는지를 확인하고 있습니다. 이 건 코드 스멜 중 Feature Envy 입니다. TripService 클래스가 User 클래스의 역할을 탐내는 거죠 User 클래스가 되고 싶은 겁니다. 여기서 Feature Envy를 풀려면 어떻게 해야지요? User 클래스가 친구 목록을 가지고 있다면 User 클래스에게 물어보면 됩니다. "이봐 이 사람이랑 친구인가?" 라고요. 그럼 이 부분을 옮기겠습니다. 일단은요 유저 클래스를 봅시다. 어디 있지요? 이 건 닫고 여기 있네요. 유저 클래스의 테스트를 작성합시다. 이 건 유저 클래스고 테스트 케이스를 새로 만들고요 테스트 경로에 넣어야겠지요 테스트 클래스가 만들어 졌고요 이 쪽으로 옮기겠습니다. 네 이제 뭘 해야지요? 친구인지 확인하는 기능을 User 클래스로 옮길 겁니다. 그냥 User 클래스에게 물어보는 거지요. "저 사람이랑 친구야?" 라고요. 테스트 먼저 작성해야겠지요 유저들이 친구가 아니면 친구가 아니라고 해야 한다 친구가 아닌 경우부터 테스트하겠습니다. 제일 쉬운 테스트이니까요 가장 간단해요 UserBuilder를 사용해야겠지요 Bob과 친구라고 합시다 그렇다면 Paul과는 친구가 아니겠지요 보시다시피 Bob과 친구인 유저를 하나 만들고 유저에게 Paul과 친구냐고 물어보는 거지요 당연히 친구가 아니겠지요 Bob과 Paul을 생성하겠습니다 Paul도 만들어야지요 이 걸 User 클래스에 추가하려고요 생성하겠습니다. 이 테스트를 통과하기 위해 필요한 최소한의 구현은 false를 반환하는 거지요. 저장 하자마자 인피니테스트가 테스트가 성공했다고 알려주네요. 멋지군요. 유저끼리 친구인 경우도 테스트하겠습니다. 유저들이 친구인 경우 친구라고 알려야 한다. 이 코드를 가져오겠습니다 밑으로 내려서 어떻게 할거냐 하면 Paul과도 친구로 해주고 그러면 당연히 true가 되겠지요 테스트를 통과하지 못했군요 실제로 구현을 해야겠지요 파라메터 이름으로 Paul은 맞지 않겠지요 anotherUser로 변경하고요 좋습니다. 메소드 구현이 되었네요. 테스트로 실행도 되고 있지요. 이 부분은 실행되지 않았지만 상관없지요. 다시 커밋하기에 좋은 타이밍이지요? isFriendsWith(user) method added to User 좋습니다. 다시 테스트로 돌아가서 커버리지 결과는 지우고요 이제 User 클래스에 새로운 행위가 추가되었습니다. 리팩토링을 할 때에는요 항상 테스트가 성공하는 상태로 작업할 수 있게 노력해야 합니다. 항상 테스트가 성공하는 상태로 작업할 수 있게 노력해야 합니다. 계획 없이 마구 고쳐버리면 안됩니다. 이것 저것 마구 고쳐서 테스트들이 마구 깨져버리면 안됩니다. 하나씩 수정해 가면서 테스트를 같이 실행해야 합니다. 인피니테스트를 사용하는 것도 좋은 방법입니다. 인피니테스트를 사용하는 것도 좋은 방법입니다. 코드를 저장할 때마다 테스트를 실행해서 바로 바로 결과를 알 수 있습니다. 뭔가 수정을 했는데 실수를 했다면 테스트 결과를 보고 바로 알아 챌 수 있습니다. 그러면 Ctrl+Z 한 번으로 복구할 수 있습니다. Ctrl+Z를 누르고 저장만하면 됩니다. 바로 문제없던 상태로 돌아갈 수 있습니다. 이제 안전하게 리팩토링을 할 수 있게 가능한 작게 변경을 해보겠습니다. 자 여기 불린 변수를 옮겨볼까요? 이 if 구문 안에서만 사용되고 있으니까요 또 다른 팁을 알려드리면요 레거시 코드로 작업을 하다 보면 변수들이 여기 저기에 선언된 경우가 있는데요 가능한 인접하게 모으세요 사용되는 위치에 가깝게 모으세요. 변수가 코드의 위 부분에서는 사용되지 않고 밑에서만 사용되고 있는데 맨 위에 선언되어있다면 사용되는 근처로 변수 선언을 옮기세요 그러면 연관된 코드들이 모여있게 되므로 코드를 분리하기에도 좋습니다. 이제 친구인지를 바로 알 수 있지요 loggedUser보다 loggedInUser가 낫군요 이렇게요 기존 코드에 작업을 하다가 이름이 이상하거나 적당하지 않으면 그 순간에 바로 이름을 바꾸세요. 좋습니다. 여기 보이죠 여기 초록색 상태바를 계속 보는 거에요 코드를 저장할 때마다 수정된 부분의 테스트가 실행돼서 결과가 표시됩니다. 이제 이 부분을 주석 처리할 수 있겠지요 저장을 하고요 테스트 결과를 보니 정상이네요. 이제 지우면 되겠네요. 이 것도 지우고요 그러면 아마도.. 이 건 인라인하고요 좋습니다. 깔끔하네요. 이 것도 금방 바꿀 수 있겠지요 이 부분이요 Guard Clause 패턴을 적용할 수 있겠지요? 이 부분을 Guard Clause로 바꿀 수 있습니다. Guard Clause는요 파라메터가 들어오자마자 유효성을 검증하는 것이지요. 그러면 이 부분을 Guard Clause로 변경해서 이 if 문을 없애보겠습니다. loggedInUser가 Null이면 예외를 던지는 거지요. 이 게 위에 있어야겠지요. 저장을 하면 바로 녹색으로 표시되네요. 친절하게도 이클립스가 이 부분은 실행되지 않는 코드라고 알려주네요. 이제 if 문을 삭제할 수 있겠습니다. 저장하고 좋습니다. 지금 리팩토링하는 동안에 한 번도 테스트가 실패하지 않고 있지요. 지금 리팩토링하는 동안에 한 번도 테스트가 실패하지 않고 있지요. 혹시 실수를 해서 실패하는 테스트가 생겨도요 실제 업무 중에는 그럴 수도 있겠지요 Ctrl+Z를 누르면 되요. 금방 이전 상태로 되돌릴 수 있습니다. 그리고 뭐가 잘못된건지 생각하면 됩니다. 테스트를 유지할 수 없는 경우도 있겠지만 가능한 피해야 합니다. 이제 코드가 두 개의 블럭으로 구분되는군요. 첫 번째 블럭은 유저 정보를 검증하고요 두 번째 블럭은 친구의 여행 정보를 조회합니다. 여기서도 조금 더 개선이 가능하겠네요 어디 봅시다 레거시 코드에서 변수를 제거하는 것도 시도해 볼 만한 것 입니다. 변수 개 수가 적을 수록 코드는 더 쉬워지거든요. 조금은 중복이 되거나 메소드를 두 번 호출하게되서 약간의 성능이 희생되는 경우에도 가치가 있습니다. 변수를 줄이는 것은요 변수는 여러 곳에서 값이 세팅되고 참조되기 때문에 긴 코드를 파악하기 어렵게 합니다. 변수를 없애고 메소드를 반복해서 호출하면 코드를 이해하기 쉬워집니다. 그런 후에 꼭 필요한 경우에만 최적화 하면 됩니다. 예를 들어 이런 식으로요. 저장을 하면 문제 없지요? 이런 식으로도 변경할 수 있을 거에요 문제 없고요 이 건 필요 없지요. 그대로입니다. 이 변수는 이제 필요 없지요 이 것도 필요 없고요 테스트는 정상이에요 삼항 연산자를 써서 조금 더 간단하게 해볼까요 예를 들어서 저장을 하면 여전히 녹색이네요. 좋습니다. 그 다음에 이 변수는 두 군데에 사용되고 있는데요 인라인 시키겠습니다. 둘 다 인라인 되었지요. 이런 식이지요. 변수를 없앴습니다. 물론 메소드를 두 번 호출하고 있어요 하지만 코드는 더 간단해졌지요. 물론 이렇게 해서 성능에 문제가 생기면 메소드 호출을 줄이도록 수정해야겠지요. 좋습니다. 훨씬 좋아졌네요. 이 부분은 마음에 들지않는군요.. 메소드로 만들겠습니다. 이렇게 하면 좋은 점은요 예를 들어서요 코드를 보면 테스트를 보면요 테스트 이름은 유저가 로그인하지 않은 경우에 예외를 던진다 로그인 된 유저를 조회한다 예외를 던진다 테스트는 유저가 친구가 아니면 여행 정보를 반환하지 않는다 유저가 로그인 유저와 친구라면 여행 정보가 없고 테스트는 유저가 친구들인 경우에 여행 정보를 조회한다 로그인 유저와 친구인 경우 유저의 여행 목록 이런 식으로 테스트에 사용하는 용어들이 제품 코드 구현에도 그대로 사용되게 됩니다. 제품 코드 구현에도 그대로 사용되게 됩니다. 꼭 이런 식으로 작성해보시길 바랍니다. 잠시 멈추고 커밋하겠습니다. TripService refactored 좋습니다. 훌륭해요. 이 정도면 충분하겠지요? 마무리할까요? 아닙니다. 아직 한참 멀었습니다. 여기 이 짧은 코드요 어마 어마한 문제점을 가지고 있습니다. 그 중 하나를 이야기하면요 우리는 지금까지 테스트가 없는 레거시 코드에 테스트를 작성했습니다. 그렇게 해서 문제 없이 코드를 수정할 수 있다는 자신감이 생겼어요. 대단한 일이지요. 하지만 반대로 단점도 있는데요 기존 코드의 디자인이 나쁜 경우에는 어떻게 될까요? 기존 코드의 디자인이 나쁜데 그 위에 테스트를 작성해 놓으면 디자인을 수정할 수 없게 만들어 버립니다. 매우 주의해야 해요 원래 디자인 변경은 쉽게 하는 일이 아닌데요 그 위에 테스트들을 얹어버리면 더 변경하기 힘들게 됩니다. 디자인을 변경하는 순간 작성했던 테스트들도 같이 수정해야 하므로 주의해야 합니다. 그럼 이 디자인은 뭐가 문제일까요? 아마도 TripService는 서버단 코드이고 (MVC 중) 모델에 속할 겁니다. 그런데 User Session을 직접 참조하고 있네요 모델이 웹 프레임워크를 참조하면 안되지요 Http Session 같은 것들을 알고 있으면 안됩니다. 이 클래스는 User Session을 직접 참조하는 게 잘못된 겁니다. 해결을 해야 할텐데요 선택할 수 있는 하나의 방법은 로그인 유저를 메소드에 인자로 전달하는 겁니다. 이제 위험한 리팩토링을 시작하겠습니다. 오늘 연습이야 별 문제 없겠습니다만 실제 업무에서 할 때에는 주의를 기울이셔야 합니다. 조심스럽게 언제나 처럼 조금씩 작업하겠습니다. 메소드 파라메터로 로그인 유저를 전달하게 할 건데요 사용할 수 있는 방법은 리팩토링에서 메소드 시그니처를 수정해서 파라메터를 추가하겠습니다. 기본 값은 Null이고요 좋습니다. 이클립스의 리팩토링 기능은 매우 강력합니다. 인텔리제이도 그렇고요. 이 유저 파라메터는 사용되고 있지는 않지요. 테스트 코드를 보면 Null을 전달하는 것으로 변경되어 있네요. 멋지군요. 여기서 문제는요 이 메소드는 제품 코드이니 이미 사용되고 있을 거에요 아마 여러 군데서 사용 중이겠지요. 따라서 사용되는 모든 곳에서 Null을 넣어주고 있을 테니 모든 곳에서 전부 수정을 해줘야 합니다. 잊지 마시기 바랍니다. 일단 지금 해야 할 것은 테스트를 수정해야지요 여기 Null을 전달하는 게 보기 싫군요. 로그인 유저를 전달해야 하니 게스트를 넣어주고요 인피니테스트 결과를 확인하고요 여기에는 로그인 된 유저를 넣어야지요 여기에는 네 여기에도 로그인 된 유저를 넣겠습니다. 좋습니다. 한 번 더 확인해보고요. 좋습니다. 이제 전달되는 파라메터를 사용해야겠지요 이제 전달된 걸 사용할거에요. 파라메터를 사용하게 변경할겁니다. 여기 메소드 호출을 파라메터로 변경하고요 저장을 하면.. 테스트 결과는 정상입니다. 그러면 이 메소드는요 private으로 변경하면 사용 안 된다고 표시가 되지요. 삭제해도 안전하겠군요. 삭제한 후에 이 것도 삭제할 수 있겠지요. 다시 정상으로 돌아왔지요. 유저를 파라메터로 전달하게 리팩토링 했습니다. 이제 할 것은 이 loggedInUser 필드는 필요할 것 같지 않네요. 이렇게 바꾸면요 여기도요 그러면 이클립스가 이 변수가 사용되지 않는다고 알려주지요. 삭제할 수 있겠군요. 삭제하고요. 이 것도요 테스트도 정상입니다. 커밋을 해야겠네요. 오. 여기 경고가 있네요 사용하지 않는 import는 지워야지요 깔끔하게요. 장인정신은 중요합니다. 항상 깔끔하게 유지해야지요. 좋습니다. 잠시 멈추고요. Logged in user passed in to TripService 좋습니다. 대충 마무리됐을까요. 이 부분은 여전히 마음에 안 드네요. TripDAO의 static 메소드를 직접 호출하고 있지요. 그래서 이런 식으로 해야만 했어요. 말했듯이 임시적인 방법인데요 제대로 한다면 TripDAO를 주입해서 멤버 변수를 사용하게 만들어야 하는데요 static 메소드라서 다른 것으로 대체하기가 어렵지요. 왜냐하면 일반적으로 인스턴스만 주입이 가능하고 클래스는 불가하기 때문에 Mock으로 대체할 수 없습니다. 그럼 조금 더 수정해서 이 부분을 없애고 TripDAO의 멤버 변수를 만들어 사용하게 해보겠습니다. TripDAO 클래스를 열어 구현 내용을 보면요 지금 이 코드는 단순한 예제 코드라 예외를 던지고 끝나지만요 단순한 예제 코드라 예외를 던지고 끝나지만요 앞에 이야기한 것처럼 보통은 데이터베이스나 분산 캐시 같은 저장하는 무언가에 엑세스하겠지요. 자 이제 멤버 메소드를 만들건대요. 먼저 테스트를 작성해야겠지요. 테스트 폴더에 넣고 이 것도 흥미로운 기법이니 눈 여겨 보세요. 기존 Static 메소드와 동일한 기능을 하는 인스턴스 메소드를 만들 거에요. 똑 같이 동작하게요. 그냥 Static 키워드를 없애도 가능하겠지요. 여기 static 글자만 지워서요 물론 가능합니다만 그렇게 하면 여기 저기 수정할 게 많을 겁니다. 어플리케이션에서 이 메소드를 사용하는 클래스가 여러 개 있는 상황에서 이걸 인스턴스 메소드로 바꿔버리면 전부 컴파일 에러가 나겠지요. 그러면 일이 너무 커져버립니다. 우린 조금씩 안전하게 작업을 할거니까요 그래서 테스트를 만들겠습니다. 예제 DAO를 테스트하는 거니까 이 테스트가 검증하는 디테일 한 부분에는 집중하실 필요는 없습니다. 그냥 예제 DAO의 동작을 검증하겠습니다. 유저의 여행정보를 조회하면 예외를 발생하는 거지요 예외가 발생해야겠지요. 말씀드린대로 원래는 데이터를 조회해야겠지만 이 예제 코드에서는 바로 예외를 던지지요. 이거지요. 인스턴스 메소드를 사용하려고 하니까요 메소드를 호출하기만 하면 되겠지요 그러니까 인스턴스가 필요하지요 메소드를 생성해서 기존 거랑 똑 같이 동작하게 할 겁니다 테스트는 실패하고 있지요. 당연합니다. 이제는요 기존 static 메소드랑 똑 같이 동작해야 하니까요 이 안에서 기존 메소드를 호출합니다. 이렇게 하는 이유는요 이렇게 하는 이유는요 점진적으로 기존 코드들을 새 메소드를 사용하게 바꿔가려는 거지요. 여기에서 TripService가 인스턴스 메소드를 호출하게 하는 거지요 이렇게 시작해서 Static 메소드를 사용하는 클래스가 없어질 때까지 계속 바꿔나가는 겁니다. 그런 후에 이 Static 메소드의 내용을 복사해서 여기로 옮기고 Static 메소드는 지워버리면 끝이지요. 유용한 기법이에요. 다시 돌아가서 인스턴스 메소드가 완성됐습니다. 이제는 TripDAO를 인젝트 해야 하는데요 방법은 여러 가지가 있습니다. 예를 들어 DI 프레임워크 없이 그냥 할 수도 있겠지요 그냥 TripService의 생성자에 TripDAO를 넣어주면 됩니다. 그렇게 하는 게 가장 간단한 방법입니다. 그래도 대부분의 자바 개발자들은 Spring, Mockito 등을 주로 사용하니까요 Mockito를 이용하는 방법을 보여드리겠습니다. 만약에 Mockito를 사용하지 않으면 그냥 심플하게 생성자를 만들어 TripDAO를 건네주면 됩니다. 그럼 Spring, Mockito를 사용해서 테스트하는 방법을 보여드리겠습니다. 이제 TripDAO를 인젝트 할 수 있도록 테스트를 변경하도록 하겠습니다. 이제 JUnit과 Mockito를 사용할 거니까 Mockito나 JUnit을 사용하지 않는 사람에게는 좀 복잡해 보일 겁니다. 그렇다면 관련 문서들을 읽어보시기 바랍니다. 지금까지는 TestableTripService을 사용하고 있었는데요 지금까지는 TestableTripService을 사용하고 있었는데요 이제 TripService를 바로 사용하겠습니다. 직접 생성하지 않고 주입 받아서요. TestableTripService를 이제 지워야겠어요 더 이상 필요가 없습니다. 의존하는 것들은 Mock으로 바꿔서 테스트할 수 있게 조금씩 안전하게 작업하겠습니다. TripService를 하나 더 만들고요 TripService를 하나 더 만들고요 일단 realTripService라고 하고 이건 진짜 TripService의 인스턴스에요 Mock을 만들고요 TripDAO의 Mock이요 Mockito 기능을 사용해서 TripService가 Mock을 사용하게 하겠습니다. Mockito가 어떻게 동작하는지는 오늘 주제에서 조금 벗어나지만 간단하게 설명 드리면 실제 TripService 인스턴스가 생성될 때에 Mockito가 TripService 클래스를 분석해서 TripService가 사용하는 Mock들을 자동으로 주입시키는 겁니다. Mock DAO는 여기에 있고요 이제 TripService가 이 Mock TripDAO가 필요하다는 것만 명시하면 됩니다. 일단 그 전에 이제 이 TripService의 인스턴스를요 조금씩 조금씩 realTripService로 바꾸고 저장.. 테스트 결과는 정상이지요 지금 하려는 거는요 테스트에서 사용 중인 TestableTripService를 조금씩 TripService 인스턴스로 바꾸는 겁니다. 이렇게 하면 정상이지요. 지금 테스트가 통과하는 이유는 TripDAO를 사용하지 않기 때문이에요. 이 코드를 사용하지 않는 테스트들인 거지요. 이 테스트도 바꾸면요.. 실패하는 군요. 이거지요. TripService 인스턴스에서 TripDAO의 Static 메소드를 호출했기 때문에 TripDAO의 Static 메소드를 호출했기 때문에 예외가 떨어진 거에요. 이제 실패한 테스트가 있으니 제품 코드를 수정해야겠네요. 그래서 제품 코드에서요 스프링인 경우지요 import 하고 TripDAO는 XML에 스프링 빈으로 등록했다고 하고요 TripDAO는 XML에 스프링 빈으로 등록했다고 하고요 좋습니다. 화면을 늘리고 이러면 Mockito가 이 Mock TripDAO를 여기 TripService에 넣어줄 겁니다. 그래서 이 TripDAO를 사용하게 해야 하는데요 테스트는 실패하고 있지요? 이 TripDAO를 사용하려면 이거 대신에 일단 테스트를 돌리면요 차이점을 보려고요 실행하고 계속 단축키를 잘 못 누르네요 아직 실제 DAO를 호출해서 에러가 나고 있는 겁니다. 그래서 인스턴스 메소드를 호출하게 하고 기존 Static 메소드 대신에 인스턴스 메소드를 사용하게 해서 테스트를 실행하면 에러가 변했지요 이제 Mock DAO를 사용하고 있는 겁니다. 화면은 줄이고 이젠 Mock DAO가 실행된 겁니다. MockDAO가 여행 정보 2개를 반환되었어야 하는데 아무것도 없었던 거지요. 원래 테스트 시나리오는 여행 정보 2개가 필요하니까요 이제.. 이렇게 되는 이유는 TipService가 Mock TripDAO를 호출하는데 Mock TripDAO가 어떻게 동작해야 할지 설정하지 않아서 에요. 그러니까 이 메소드가 호출이 되면 친구의 여행 목록을 반환해야지요 지금 하는 건 Mock DAO가 여행 목록을 반환하게 하는 거지요 이 코드가 했던 것과 동일하게 Mock이 동작하도록 하는 겁니다. 이제 테스트가 통과하는군요 이제 이걸 private로 바꿀 수 있겠지요 이 메소드는 이제 필요가 없고요 전체 테스트를 다시 돌리면 모두 정상이군요. 이 클래스가 더 이상 필요 없다는 말이지요 그건 이 것도 필요 없다는 거고 이 게 필요 없으면 이 것도 필요 없지요 이제 이 필드는 사용되지 않네요 삭제하고요 그러면 이제 realTripService는 이름을 바꿔야겠지요 이제 real은 의미가 없으니까요 테스트를 작성할 때에 testee, mock 같은 이름을 사용하는 경우가 종종 있는데요 실제로 무엇인지를 표현하면 됩니다. 이건 TripService이니까 그냥 tripService로 충분합니다. 그렇지요? 좋습니다. 이제 Test Double 클래스들도 필요 없고 테스트도 읽기 편합니다. 테스트를 한 번 더 돌려서 확인하고요. 여기는 이렇게 바꿀 수 있겠지요. 앞에 이야기한 것처럼 테스트 메소드들을 접으면 이렇게 되는 거지요. 유저가 로그인하지 않은 경우 예외를 던진다 여기에서 로그인 정보를 확인하지요 유저들이 친구가 아닌 경우 여행 목록을 반환하지 않는다 친구가 아닌 경우에는 빈 목록을 반환하고요 유저가 서로 친구인 경우에는 여행 정보를 반환하지요 로그인 유저와 친구인 경우에 여행 목록을 반환합니다. 이 getter 메소드는 좀 별로네요 getTripsByUser는요 getFriendTrips로 훨씬 낫지요? 이 변수도 friend가 좋겠네요 이런 식이지요 테스트가 잘 작성되어있으므로 이런 것들을 해볼 수가 있는 겁니다. 잠시 멈추고 커밋을 해볼까요. TripDAO injected into TripService 됐습니다. 이런 식으로 레거시 코드를 안전하게 리팩토링 할 수 있습니다. 레거시 코드를 안전하게 리팩토링 할 수 있습니다. 처음 TripService 코드가 여기에 있을 텐데요. 처음에는 이런 코드로 시작했던 겁니다. 처음에는 이런 코드로 시작했던 겁니다. 지금은 스크린 캐스트를 녹화하고 있으니 한 시간 정도 걸렸는데요 여기에 사용된 기법들이 익숙한 경우에는요 저 같은 경우에는 평소에는 20분 정도 걸립니다. 뭔가 새로운 기술을 알게 되면 연습을 하세요 능숙하게 사용할 때 까지요 다시 한 번 말씀 드리면 레거시 코드에 작업을 할 때에는 깊은 부분부터 작업을 하면 안됩니다. 항상 얕은 부분부터 시작을 하세요. 얕은 부분에서 시작해서 깊은 부분으로요. 얕은 부분에서 테스트를 작성하기 시작해야 합니다 그러면 작성하는 테스트로 제품 코드를 이해해갈 수 있고 테스트 데이터를 만들어 가면서 점점 더 깊은 부분을 테스트할 수 있게 됩니다. 반대로 리팩토링을 할 때에는 가장 깊은 부분부터 시작합니다. 깊은 부분부터 메소드를 추출하고 행위를 다른 클래스로 옮기다 보면 어느 순간부터 코드가 줄어들기 시작할 겁니다. 읽기 좋고 관리하기 좋은 코드를 작성하세요. 도메인 언어가 코드에 사용되도록 하세요. 코드에 녹아들 게 하세요. 도메인 언어가 테스트와 제품 코드에 녹아들 게 하세요 BA와 이야기하는 용어가 테스트 코드 제품 코드에도 그대로 사용되게 하세요. 심플한 것을 추구하고 사용하는 프레임워크나 단축키 등도 알아두세요 신속하게 작업하도록 노력하고 개발 환경에도 공을 들여서 단축키 등으로 간단하게 메소드나 클래스를 만들거나 리팩토링을 할 수 있어야 합니다. 항상 조금씩 점진적으로 작업하고요 자주 커밋하세요. Git같은 툴을 사용하면 실수를 하더라도 Ctrl+Z 나 git reset hard 등으로 이 전 커밋으로 돌아갈 수 있습니다. 문제가 없었던 상태로요. 용기를 가지세요. 오늘 여러 가지 기법들을 배웠는데요 오늘 여러 가지 기법들을 배웠는데요 이제 레거시 코드의 99%에 테스트 작성이 가능할 겁니다. 그러니 용기를 내서 오늘 배운 것들을 활용하세요. 머문 자리는 머물기 전보다 깨끗하게 하고 떠나세요. 보이 스카웃 규칙이지요. 깨진 창문 이론도 아시지요. 코드 냄새가 나면요. 코드가 그다지 잘 작성된 게 아니라면 바로 리팩토링 하세요. 바로 수정하세요. 유익한 시간이 됐으면 좋겠습니다. 이상입니다. 이 카타는 제 깃헙에 있습니다. 제 깃헙을 방문해보세요. 이상입니다. 유익한 시간이 되었으면 좋겠습니다. 감사합니다.