-
처음으로 해본 리팩토링 후기Spring Framework 2022. 3. 24. 02:40반응형
1) 서론
일이 바쁘다는 핑계로 사용한 물건을 집안 곳곳에 아무렇게나 방치한 경험이 있나요?
당시의 나는 일단 물건을 어딘가에 두고, 씻고 잠자는 것이 더 편하고, 우선이라고 생각했을 것 같습니다.
하지만 시간이 흐르고 이러한 것들이 쌓이게 된다면, 물건을 찾기는 더더욱 불편해집니다. 어쩌다 새로운 가구를 구매라도 하게 되면, 어디에 놓아야 할지 엄두가 나지 않습니다. 왜냐하면 정리되지 않은 물건들로 공간은 가득할 테니까요.
이런 순간이 되면 미리미리 치워놓을걸 하는 후회와 함께, 이 짐들을 어디로 옮기고, 새로운 가구는 어떻게 배치할 것인지 고민이 될 것입니다.
아마도 우리가 작성하는 코드도 비슷할 거라고 생각합니다. 처음에는 빠르게 개발하는 것이 중요했을 것이고, 그것이 실제로 중요한 시기였을 것입니다. 그때는 그 방법이 최선이었을 테니까요.
그럼에도 불구하고, 기능의 변경과 추가가 발생할 때 빠르게 쌓아 올린 기존의 코드들이 문제 되는 시기가 오는데요. 처음으로 이러한 순간을 마주하고, 리팩토링을 진행하며 느낀 점입니다.
(단순히 후기성 글이며, 코드에 대한 내용은 없습니다)
2) 리팩토링을 시작한 계기
특정 데이터를 스크래핑 후 배치 처리하는 로직이 있었습니다. 해당 로직에서 오류가 지속적으로 발생하고 있었고, 정상적으로 동작하지 않고 있었습니다.
for문 try ~ if 문 ~ for ~ 객체 생성 DB 조회 무슨 무슨 로직 무슨 무슨 로직2 DB 데이터 삭제 DB 데이터 삽입 로그성 데이터 DB 삽입 catch ~ finally ~ 다시 반복
그중에서도 아래의 두 개가 가장 크게 느껴진 리팩토링의 필요성이었는데요.
2. 1) 하나의 큰 덩어리로 이루어진 메서드
스크래핑 및 배치 처리의 모든 로직이 하나의 메서드에 단순히 나열되어있었습니다.
하나의 큰 덩어리로, 단순히 나열된 로직은 아래와 같은 문제점이 있었습니다.
- 강결합
- 가독성
첫 번째로 로직 간의 결합도가 높았습니다. 즉 하나의 로직을 변경하기 위해서는 다른 로직이 반드시 변경되어야 했습니다. 그러다 보면, 전체를 다시 만드는 것과 같은 수준의 변경이 발생할 수밖에 없었습니다.
그리고 단위 테스트를 할 수 없었습니다. 단위 테스트는 가장 작은 단위의 모듈을 독립적으로 테스트하는 것입니다. 하지만 로직 간의 결합도가 높아 단위를 구분 지을 수 없었고, 독립적으로 하나의 모듈만 테스트하는 것이 불가능했습니다.
단순히 해당 기능들이 잘 동작하는지 보기 위해서는 애플리케이션 전체를 빌드 후 실행시켜야 했는데요. break point를 하나하나 찍어가며 디버깅을 하는 방법밖에 없었습니다.
추가적으로 로직 간의 오류 발생 시 예외처리 방법은 다 다를 것입니다. 하지만 하나의 메서드에 단순히 나열된 코드의 예외처리는 매우 어렵습니다. 그냥 전체를 try - catch로 묶어서, 일괄적으로 처리하는 것이 가장 쉬운 방법일 것 같습니다. 하지만 전체를 try - catch로 묶는 것은 각 로직에 맞는 예외처리를 할 수 없다는 점이 문제점입니다.
두 번째로 개인적으로 생각하는 가장 큰 단점인데요. 가독성입니다. 기능 단위로 분리하게 되면, 표현 가능한 메서드명을 가지고 어떤 동작을 하는지 예측하고 나타낼 수 있습니다. 하지만 단순히 나열된 코드들은 어떠한 동작을 하고자 하는지 쉽게 읽히지 않고, 예측하기 어렵습니다.
2. 2) 성능상 문제점
코드를 설명하는 글이 아니기에, 대표적인 문제점 하나만 간략히 언급합니다.
위의 로직은 For loop 안에서 지나치게 많은 동작들이 반복되고 있었습니다.
그래서 아래의 문제점들이 있습니다.
- 큰 데이터를 담는 객체의 반복적인 생성, 해제
- 반복적인 DB connection 연결, 해제
- 반복적인 validation 확인 등
3) 개선
첫 번째로 모든 로직을 기능 단위로 분리했습니다. 하나의 큰 메서드에서 약 20개 정도의 메서드로 분리가 됐는데요.
덕분에 전체적인 로직의 결합도가 낮아졌습니다. 하나의 기능이 동작하는지 확인하고 싶을 땐 독립적으로, 그것만 테스트하면 됩니다. 특정 기능의 수정이 필요할 땐 해당 부분만 수정하면 됩니다. 다른 로직에게 영향을 미치지 않습니다. 마지막으로 다른 비슷한 스크래핑 및 배치 처리에서 기능 단위로 재활용할 수 있게 됐습니다.
두 번째로 for loop에 반복되는 연산을 최소한으로 줄였습니다.
대량의 데이터를 담는 객체를 for loop 밖으로 뺐습니다. 그리고 for loop을 돌면서 저장될 모든 데이터를 담은 뒤 일괄적으로 DB update 하는 방법을 사용했습니다.
덕분에 데이터를 담는 불필요한 객체의 생성과 해제를 방지하고, DB connection의 연결 및 해제를 최소화할 수 있었습니다. 이 과정에서 ArrayList, HashSet의 initial capacity를 변경해서, 시간 복잡도를 줄이기도 했습니다.
물론 이러한 방법은 배치 처리를 어떤 환경에서 하느냐에 따라서 다를 수 있습니다. 하나의 DB를 가지고 사용자의 요청과 배치 처리를 일괄적으로 하다 보면, DB의 connection을 얻지 못하는 이슈가 발생할 수도 있습니다. 오히려 이럴 때는 적은 단위의 데이터를 빠르게 insert 후 DB connection을 다른 요청에게 반환하는 것이 좋다고 생각합니다.
결과적으로 성능은 4배 향상됐고, 기존에 누락된 30만 건의 데이터가 성공적으로 스크래핑됐습니다.
4) 리팩토링을 통해 배운 점
처음으로 리팩토링을 경험하며, 많은 것을 느끼고 배웠던 것 같습니다.
두 가지 후회되는 점이 있습니다.
첫 번째는 시간을 너무 소비했습니다. 약 일주일반 정도의 시간을 투자했습니다. 결과물을 놓고 봤을 때는 좋다고 생각할 수도 있습니다.
하지만 반대로 생각하면, 작은 스타트업에서 서비스에 반영되지 않는(merge 되지 않는) 코드를 1주일 반 동안 들고 있었다는 것입니다. 일주일반 동안 고객에게 영향을 미치는 서비스 개선이 없었다는 이야기와 같다고 생각합니다.
그동안 애자일, 스프린트 방식의 업무를 경험한 적이 없어서, 장단점을 체감해본 적은 없는데요. 이번 리팩토링을 통해서 왜 많은 회사들이 애자일, 스프린트 방식을 도입하는지에 대한 필요성은 조금은 이해가 됐습니다. 작은 주기를 바탕으로, 작은 단위의 서비스 개선을 빠르게 테스트하고 적용시키는 것이, 어찌 보면 회사의 성장에는 더 도움이 되는 것 같다는 생각도 듭니다.
두 번째는 한 번에 너무 큰 단위의 기능을 변경하려고 했습니다. 위의 내용과 조금 연관된 이야기인데요.
한 번에 큰 단위의 기능을 변경하다 보니 PR의 변경사항은 점점 커지고, 코드를 수정하는 시간은 계속 늘어났습니다. 특히 변경사항이 너무 많아서, 리뷰를 해주는 사람들도 고통스러웠을 것입니다. 테스트 코드 제외하고, 약 1천 라인이 변경됐습니다. 작은 단위 PR의 중요성을 다시 한번 깨닫습니다.
특히 코드를 수정하는 시간이 늘수록 마치 늪에 빠지는 듯한 경험을 했습니다. 그러다 보니 쉽게 생각할 수 있는 부분을 어렵게 돌아가는 경험도 했었습니다. 혼자서 하루 종일 고민하는데, 옆의 개발자가 "이렇게 하면 되지 않아?"라고 할 때면, "아! 이걸 왜 생각 못 했지" 이런 생각이 들기도 했습니다. 코드에만 빠져 너무 깊게 보다 보니, 오히려 시야가 좁아지는 느낌이었습니다.
전체적으로는 단위 테스트의 중요성도 배울 수 있었습니다.
입사 때 팀 내 개발자분이 말씀해주신 게 생각납니다. "결합도가 높은 코드를 만나게 되면, 왜 단위 테스트를 작성해야 하는지 알 것이다"라고요.
TDD 같은 테스트 주도 개발을 말하는 것이 아니었습니다. 단위 테스트를 작성할 수 조차 없는 코드는 결합도가 아주 높은 코드이니, 개선을 해야 한다는 말이었습니다. 즉 단위 테스트와 함께 코드를 작성하다 보면 자연적으로 결합도가 낮고, 기능 단위로 잘 분리된 코드를 작성할 수 있다는 것을 느꼈습니다.
종합적으로 보면 생각보다 힘든 리팩토링 과정이었지만, 스스로 많이 배울 수 있는 좋은 경험이었다고 생각합니다.
반응형'Spring Framework' 카테고리의 다른 글
Querydsl cross join 개선 하기 (0) 2022.06.18 FK 없는 OneToOne 즉시 로딩 개선하기 (1) 2022.06.07 JPA cascade 멈춰! (2) 2022.02.02 @Transactional rollback과 테스트 의문점 (0) 2022.01.24 JPA 지연 로딩, 결국 Proxy (0) 2021.11.21