티스토리 뷰

반응형

안녕하세요~ 오늘은 개발자의 숙명과도 같은 코드 리팩터링에 대해 알아보고, 코드 리팩터링 방법에 대해 기술해보려 합니다. 가장 먼저 리팩터링의 정의를 먼저 알아야 합니다.

 

리팩터링이란 ? 

리팩터링(refactoring)은 소프트웨어 공학에서 '결과의 변경 없이 코드의 구조를 재조정함'을 뜻한다. 주로 가독성을 높이고 유지보수를 편하게 한다. 버그를 없애거나 새로운 기능을 추가하는 행위는 아니다. 사용자가 보는 외부 화면은 그대로 두면서 내부 논리나 구조를 바꾸고 개선하는 유지보수 행위이다.

리팩터링의 잠재적인 목표는 소프트웨어의 설계, 구조 및 구현을 개선하는 동시에 소프트웨어의 기능을 보존하는 것이다. 리펙터링은 코드의 가독성을 향상시키고 복잡성을 감소시키는 효과를 가지며, 이러한 이점은 소스 코드의 유지 보수성을 개선하고 확장성을 개선하기 위해 더 단순하고, 깔끔하거나, 표현력이 뛰어난 내부 아키텍처 또는 객체 모델을 만들 수 있게 한다. 그리고 소프트웨어 엔지니어는 더 빠르게 수행되거나 더 적은 메모리를 사용하는 프로그램을 작성해야 하는 지속적인 과제에 직면해 있기에 성능 향상이 리팩터링의 또다른 목표가 된다.

일반적으로 리팩터링은 일련의 표준화된 기본 마이크로 리팩터링을 적용하는데, 각 리팩터는 소프트웨어의 동작을 보존하거나 최소한 기능적 요건에 대한 준수를 수정하지 않는 컴퓨터 프로그램의 소스 코드의 작은 변화이다. 많은 개발 환경에서 이러한 기본 리팩터링의 기계적 측면을 수행하기 위한 자동 지원을 제공한다. 코드 리팩터링을 잘 수행하면 소프트웨어 개발자가 기본 논리를 단순화하고 불필요한 수준의 복잡성을 제거하여 시스템의 숨겨진 또는 유휴 버그나 취약성을 발견하고 해결하는 데 도움이 될 수 있다. 그러나 잘못 수행되면 외부 기능을 변경하지 않거나 새로운 버그를 도입하거나 둘 다에 대한 요구 사항을 충족하지 못할 수 있다.

출처 : https://ko.wikipedia.org/wiki/%EB%A6%AC%ED%8C%A9%ED%84%B0%EB%A7%81

 

리팩터링 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 리팩터링(refactoring)은 소프트웨어 공학에서 '결과의 변경 없이 코드의 구조를 재조정함'을 뜻한다. 주로 가독성을 높이고 유지보수를 편하게 한다. 버그를 없애

ko.wikipedia.org

 

그럼 리팩터링을 하는 이유는 알았으니, 어떻게 진행을 하는지 방법에 대해 알아보아야 할 것입니다. 가장 대표적으로 추천하는 방법은 교보문고나 알라딘 같은 책방에 들려 '리팩터링 2판' 을 사서 직접 보는 것입니다. 리팩터링 1판은 자바 코드에 대한 기본적인 리팩터링 방법과 클린 코드를 유지하는 법을 알려주고 있고, 리팩터링 2판의 경우는 자바스크립트를 기반으로 리팩터링을 진행하는 방법을 알려주고 있습니다. 

https://product.kyobobook.co.kr/detail/S000001810241

 

리팩터링 | 마틴 파울러 - 교보문고

리팩터링 | 개발자가 선택한 프로그램 가치를 높이는 최고의 코드 관리 기술 마틴 파울러의 『리팩터링』이 새롭게 돌아왔다.지난 20년간 전 세계 프로그래머에게 리팩터링의 교본이었던 이 책

product.kyobobook.co.kr

리팩터링 1판은 현재 구매가 불가능한가 봅니다. 저는 이전에 확인했던 책이었습니다.

 

현재 구매가 가능한 책은 리팩터링 2판으로 주로 자바스크립트 소스 코드를 변환하는데 주로 초점을 맞추고 있어서, 프론트엔드 개발자분들이 보고 적용하기 훨씬 쉬울 것이라고 생각이 듭니다. 

그래서 이번 글에서는 자바 코드 내에서 소스를 리팩터링하는 방법에 대해 알아보려 합니다. 저는 카카오톡의 서버 개발자님의 리팩터링 방법을 응용해서 현재 회사 및 개인 프로젝트에 도입해서 사용하고 있는 중인데, 생각보다 어렵기도 하고 예제를 찾는 것이 쉽지 않아 원문 글의 내용을 주로 참고를 많이 하였고, 이해가 금방 되는 장점이 있어 가져오게 되었습니다 :) 

 


 

 

1. 가변 상태 Context 사용 시 문제점

가변 상태를 가지는 Context 클래스가 2, 3개도 아닌 10개가 넘어가게 되면 유지, 보수 측면에서 문제가 될 수 있다고 생각합니다. 필요 이상으로 많은 Context 클래스들이 서로 물고 물리는 종속성을 가지면, 각기 다른 클래스들이 서로 변수를 넘겨주고 넘겨받는 상황이 일어납니다. 이때, 가변 Context의 레퍼런스가 다양한 함수로 전달되면서 전역 변수처럼 사용되게 되고, 이 가변 Context를 어딘가에서 A가 set을 하고, 다른 곳에서는 B가 get을 하는 상황이 발생하게 됩니다. 이런 상태에서는 코드를 읽고 동작을 이해하는 게 어려워집니다. 결국 Context를 수정해야 하는 상황이 오면, Context를 사용하는 모든 사용처를 추적하기가 어려워서, 코드를 수정하는 게 어려워지게 되고, 운영 과정에서 문제가 발생하면 디버깅도 역시 어려워진다는 문제가 생깁니다.

다른 것보다 시급했던 이 문제를 먼저 해결하기로 결정하였습니다. Context 클래스를 정리하는 리팩토링을 점진적으로 진행하여, 전체에서 사용하고 있던 10개의 Context 클래스를 현재 3개로 줄일 수 있었습니다. 코드 측면에서 볼 때, 수백 라인의 코드를 삭제하였지만, 그 기능은 동일하게 유지할 수 있었습니다.

Context를 정리하는 리팩터링 과정을 보다 상세하게 설명드리고 싶지만, 아쉽게도 사내 프로덕션 코드를 예시로 보여드릴 수 는 없습니다. 그래서, 어떠한 상황에서 Context를 어떻게 수정했는지에 대한 이해를 높이기 위해, 비슷한 상황을 담은 3가지의 간단한 슈도코드 예제를 가지고 정리해 보겠습니다. 현업에서 레거시 코드를 많이 다루었던 분들이라면 아시겠지만, 실제 프로덕션 코드에서 Context가 과도하게 사용되는 상황은 주로 레거시 코드에서 마주치기 쉽고, 결합도가 예제보다 매우 높은 것이 일반적입니다. 실무에서 다루는 레거시 코드는 굉장히 복잡해서  코드를 따라가며 각 종속 관계를 확인하고 정리할 내용을 확인하는 시간이, 코드를 수정하는 시간보다 더 많이 들 정도인 경우도 있습니다. 이 글에서는  Context 클래스를 수정하는 방법을 좀 더 이해하기 쉽도록 단순화한 예시와 함께 설명드리겠습니다.

다음은 예제로 사용할 Context 클래스입니다. 제가 리팩토링을 진행하였던 10개 Context 모두에 대한 예시를 들 수는 없으므로, 아래와 같이 방어 로직 및 예외 처리가 생략된, 컴파일되지 않는 슈도코드를 가져왔습니다.

class CarContext {
  private Car car;
  private List<People> passengers;
  private List<People> visitors;
  private SomethingBig somethingBig
  private BlahBlah
  ... 많은 필드들

  ... 생성자
  ... 다양한 get/set 들
}
 
 

 

2. 함수가 Context 객체에 set을 목적으로 Context 객체를 파라미터로 받지 말고 리턴으로 처리하자

코드 작성 시에 구현이 편하다는 이유로 Context를 객체를 파라미터로 받으면, 결합도가 높아지고 유지 보수하기 어려운 코드가 만들어집니다. 아래와 같이, 함수 내부에서 만드는 새로운 값 또는 상태를 함수의 안에서 외부 객체의 상태 변경에 직접 적용하는 것은 좋지 않습니다. 가능하면 리턴으로 받아서 처리하는 방식이 좋습니다.

Result createCar(CarContext carContext, Something A, Something B) {
  ...
  carContext.setCar(new Car());
  ...
  return new Result(someValue);
}
 

그러므로, 아래처럼 CarContext의 의존성을 코드의 큰 변경 없이 제거할 수 있습니다. Pair 리턴 또한 아름답게 구현되지는 않았지만, Context를 함수 내부에서 직접 set을 하는 것보다는 좋은 구현 방법입니다. 함수에서 리턴 받을 내용이 많아서 Pair로 해결이 안 된다면, data class 형식을 생각해 볼 수도 있겠지만, 그보다는 함수를 기능별로 좀 더 세분화하는 것을 먼저 검토하는 것이 좋겠습니다.

Pair<Result, Car> createCar(Something A, Something B) {
  ...
  return new Pair<>(new Result(someValue), new Car());
}
 
 

3. 함수에서 값을 받을 때 편하다고 Context 객체를 파라미터로 받지 말고, 필요한 것만 명시적으로 받자

함수를 사용하는 입장에서는, Context 객체를 파라미터로 받는 함수가 Context 객체의 어떤 내용에 접근하는지, 혹은 무엇을 수정하는지 알 수 없습니다. 특히, Context가 불변 클래스가 아니라면 함수를 사용할 때  Context에 영향을 미칠 수도 있기 때문에 더더욱 불안합니다.

void doSomethingForPassengers(CarContext carContext, Something A, Something B) {
  List<People> passengers = carContext.getPassengers();
  SomeValue someValue = carContext.getSomeValue();
  …
}
 

이를 보완하기 위해서는, 위와 같이 Context를 파라미터로 받아서 꺼내 사용하는 방법 대신, 아래와 같이 Context에서 필요한 것만 명시적으로 받는 방법이 좋습니다. 이렇게 하면, 함수의 의도가 명확해지고, 코드 간 결합도는 줄어들게 됩니다. 이런 방법을 사용했을 때는 파라미터 개수가 계속 증가하는 것 같아서 마음이 불편할 수 있습니다. 그럴 경우에는 일단 외부 종속을 끊고, 함수를 더 작은 책임 단위로 나누어 구현하는 것을 검토하는 게 좋습니다. 함수는 한 번에 하나의 목적만을 수행하는 게 좋기 때문입니다.

void doSomethingForPassengers(List<People> passengers, SomeValue someValue, Something A, Something B) {
  ...
}
 
 

4. 루프 최적화를 위해서 캐싱하고 싶다면 Context에 넣지 말고, 루프 밖으로 뺄 수 있는지부터 보자

아래의 복잡한 코드는 DB에서 읽은 값을 메모리에 올려놓고 계속해서 사용하려는 의도에서 구현되었습니다. 의도는 좋지만, 다른 방법으로 구현하는 것이 좋습니다.

void prepareVisitors(CarContext carContext, Condition condition) {
  if (carContext.getVisitors() == null) {
    // 메모리에 올리기 전이면 DB에서 읽는다.
    carContext.setVisitors(readFromDB(condition));
  }
}

void doSomeProcess(CarContext carContext, Condition condition) {
  ...
  prepareVisitors(CarContext carContext, Condition condition);
  ...
  List<People> visitors = carContext.getVisitors()
  ...
}

// CarContext carContext는 새로 생성되어서 넘어온다. 즉, CarContext::visitors는 비어있으니 DB에서 읽어서 Context에 넣고 재사용한다.
void doSomethingForVisitors(CarContext carContext, List<Something> somethings, Condition condition) {
  for (Something something : somethings) {
    doSomeProcess(carContext, condition);
    ...
    List<People> visitors = carContext.getVisitors();
    ...
  }
}
 

위와 같은 흐름에서 prepareVisitors() 함수에서 구현된 것처럼, DB에서 읽은 값을 메모리에 올려놓고 사용하려고 Context에 캐싱 책임을 추가하는 것보다는 해당 부분을 반복 호출 밖의 지역 변수로 꺼내는 것을 먼저 검토하는 것이 좋습니다. 만약, 꺼낼 수 없는 상황이라도 캐싱을 위한 변수를 Context에 넣지 말고 다른 클래스를 만듭시다.

이제 다음과 같이 doSomethingForVisitors()에서 사용하지 않는 CarContext 파라미터를 삭제하고, 불필요해진  CarContext::visitors getset 메서드를 삭제하여 리팩토링 합니다.

public void doSomeProcess(List<People> visitors) {
  ...
}

public void doSomethingForVisitors(List<Something> somethings, Condition condition) {
  List<People> visitors = readFromDB(condition);

  for (Something something : somethings) {
    doSomeProcess(visitors);
    ...
  }
}

 

단순한 내용을 길게 정리하게 되었습니다. 요약하면 다음의 2가지를 주의하여 Context를 사용하면 좋습니다.

  • Context 클래스를 전역 변수 저장소처럼 사용하지 않습니다.
  • 되도록 Context 객체를 파라미터로 함수에 전달해서 사용하지 않습니다.

 

작성하다보니 글이 꽤나 길어지는 관계로 3부에 나누어서 작성하려고 합니다 :) 

 

출처 : https://tech.kakao.com/2023/01/19/kakaotalk-java-app-server-refactoring/

 

카카오톡 Java App Server Refactoring 후기

안녕하세요, 카카오톡 메시징 파트에서 메시징 서버를 개발하고 있는 Soo입니다. 취미가 직업이 된 지 어느덧 8,000일이 넘어가고 있는 개발자입니다. 2019년 말에 톡 메시징 파트에 합류하여 기술

tech.kakao.com

 

반응형
댓글
공지사항