본문 바로가기
Programming/Java

Java 리팩토링(Refactoring)의 7가지 방법과 예제

by 우공80 2023. 3. 24.
728x90

 

Java Refactoring


리팩토링(Refactoring)은  코드의 품질과 효율성을 유지하기 위한 필수 프로세스입니다. 현재 프로젝트가 legacy 코드를 준용하고 있어서, 리팩토링이 필요합니다. 이 포스팅에서는 코드를 리팩토링 하기 위한 몇 가지 방법과 예시를 다뤄보려고 합니다. 아직은 부족하지만, 새로운 방법을 발견하고 학습할 때마다 지속적으로 업데이트할 생각입니다.


코드 리팩토링 방법과 예제

 

1. 변수 선언과 초기값을 assign 하는 부분이 분리되어 있을 필요가 없습니다.

 

Before:

String ssId = "";
long amt = 0L;

ssId = svcDto.getSsId();
amt = svcDto.getAmt().longValue();

After:

String ssId = svcDto.getSsId();
long amt = svcDto.getAmt().longValue();

2. 간단한 if-else 문은 삼항 연산자로 대체합니다.

삼항 연상자를 사용하면 아래와 같은 장점이 있습니다.

  1. 간결성: 삼항 연산자는 특히 논리가 단순할 때 if-else 문보다 더 간결한 코드를 허용합니다. 이렇게 하면 코드를 더 읽기 쉽고 이해하기 쉽게 만들 수 있습니다.
  2. 표현력: 삼항 연산자는 코드를 보다 표현력 있고 자체 문서화할 수 있습니다. 삼항 연산자를 사용하면 코드의 의도를 보다 명확하고 간결하게 표현할 수 있습니다.
  3. 유지보수성: 삼항 연산자는 작성하고 읽어야 하는 코드의 양을 줄임으로써 코드를 보다 유지보수하기 쉽게 만들 수 있습니다. 이렇게 하면 오류 가능성이 줄어들고 향후 코드를 더 쉽게 수정할 수 있습니다.
  4. 성능: 어떤 경우에는 삼항 연산자가 if-else 문보다 더 빠를 수 있습니다. 더 적은 명령을 사용하고 컴파일러에 의해 최적화될 수 있기 때문입니다.

Before:

if(svcDto.getSsId() != null)
{
    calcDtl.setRealSsId(svcDto.getRealSsId());
}
else 
{
    calcDtl.setRealSsId(svcDto.getSsId());
}

 

After:

calcDtl.setRealSsId(svcDto.getSsId() != null ? svcDto.getRealSsId() : calcDtl.setRealSsId(svcDto.getSsId());

 

3. if-else 문을 switch로 전환합니다.

if-else문에 비해 switch문이 나은 몇 가지 이유는 다음과 같습니다.

  1. 가독성: switch 문은 특히 평가할 조건이 여러 개인 경우 복잡한 if-else 문보다 더 읽기 쉽습니다. Switch 문은 일반적으로 코드에서 사용할 수 있는 옵션 목록을 명확하게 제시하기 때문에 이해하기 쉽습니다.
  2. 성능: 경우에 따라 switch 문이 if-else 문보다 빠를 수 있습니다. 이는 일반적으로 switch 문이 해당 코드 블록에 대/소문자 레이블을 매핑하는 조회 테이블인 점프 테이블로 구현되기 때문입니다. 이것은 일련의 if-else 조건을 평가하는 것보다 빠를 수 있습니다.
  3. 유지 관리: switch 문은 복잡한 if-else 문보다 유지 관리가 더 쉬울 수 있습니다. 새 옵션을 코드에 추가해야 하는 경우 코드의 다른 부분에 영향을 주지 않고 switch 문에 쉽게 추가할 수 있습니다. 반대로 if-else 문에 새 조건을 추가하려면 기존 코드를 수정해야 할 수 있습니다.

그러나, 성능 측면은 크게 고려사항은 아니고, 조건이 복잡하거나, 중첩적으로 사용되어야 할 때는 if-else문을 사용하는 것이 적절할 수 있습니다.

 

Before:

if(DataDef.CD_100.equals(refDto.getDcCd())) 
{
	refDto.setAmt(100);	
	break;
} 

if(DataDef.CD_200.equals(refDto.getDcCd())) 
{
	refDto.setAmt(100);	
	break;
} 

if(DataDef.CD_300.equals(refDto.getDcCd())) 
{
	refDto.setAmt(100);	
	break;
}

 

After:

switch(refDto.getDcCd()) {
    case DataDef.CD_100:
        refDto.setAmt(100);
        break;
    case DataDef.CD_200:
        refDto.setAmt(100);
        break;
    case DataDef.CD_300:
        refDto.setAmt(100);
        break;
    default:
        // handle the case where refDto.getDcCd() doesn't match any of the cases
}


4. 긴 메서드는  분해합니다.

긴 메서드는 이해하고 유지 관리하기 어려울 수 있습니다. 긴 메서드를 리팩토링 하려면 더 작고 관리하기 쉬운 메서드로 나눌 수 있습니다. 이렇게 하면 코드를 더 쉽게 읽을 수 있을 뿐만 아니라 테스트 및 디버그도 더 쉬워집니다.

Before:

public void processOrder(Order order) {
  // Do some validation
  if (!order.isValid()) {
    throw new IllegalArgumentException("Invalid order");
  }

  // Do some processing
  if (order.isProcessed()) {
    // Do some more processing
    if (order.isPaid()) {
      // Update the database
      Database.updateOrder(order);
    } else {
      throw new IllegalArgumentException("Order not paid");
    }
  } else {
    throw new IllegalArgumentException("Order not processed");
  }
}

After

public void processOrder(Order order) {
  validateOrder(order);
  processOrderDetails(order);
  Database.updateOrder(order);
}

private void validateOrder(Order order) {
  if (!order.isValid()) {
    throw new IllegalArgumentException("Invalid order");
  }
}

private void processOrderDetails(Order order) {
  if (!order.isProcessed()) {
    throw new IllegalArgumentException("Order not processed");
  }

  if (!order.isPaid()) {
    throw new IllegalArgumentException("Order not paid");
  }

  // Do some more processing
}

5. 중복 코드를 제거하고 메서드를 분리합니다.

중복 코드는 불일치와 오류를 유발할 수 있습니다. 중복 코드를 제거하려면 재사용 가능한 메서드를 만들거나 상속을 사용하여 클래스 간에 공통 코드를 공유할 수 있습니다.

Before:

public void updateCustomer(Customer customer) {
  if (customer.getStatus().equals("Active")) {
    // Do some processing
    sendNotification(customer.getEmail());
    Database.updateCustomer(customer);
  } else if (customer.getStatus().equals("Suspended")) {
    // Do some processing
    sendNotification(customer.getEmail());
    Database.updateCustomer(customer);
  } else if (customer.getStatus().equals("Inactive")) {
    // Do some processing
    sendNotification(customer.getEmail());
    Database.updateCustomer(customer);
  }
}

public void deleteCustomer(Customer customer) {
  if (customer.getStatus().equals("Active")) {
    // Do some processing
    sendNotification(customer.getEmail());
    Database.deleteCustomer(customer);
  } else if (customer.getStatus().equals("Suspended")) {
    // Do some processing
    sendNotification(customer.getEmail());
    Database.deleteCustomer(customer);
  } else if (customer.getStatus().equals("Inactive")) {
    // Do some processing
    sendNotification(customer.getEmail());
    Database.deleteCustomer(customer);
  }
}

After:

public void updateCustomer(Customer customer) {
  processCustomer(customer);
  Database.updateCustomer(customer);
}

public void deleteCustomer(Customer customer) {
  processCustomer(customer);
  Database.deleteCustomer(customer);
}

private void processCustomer(Customer customer) {
  // Do some processing
  sendNotification(customer.getEmail());
}

6. 디자인 패턴을 사용합니다.

디자인 패턴은 일반적인 소프트웨어 디자인 문제에 대한 입증된 솔루션입니다. 디자인 패턴을 사용하면 복잡한 코드를 단순화하고 더 쉽게 이해하고 유지 관리할 수 있습니다.

아래의 예시에서 리팩토링 하기 전에 MySingleton 클래스는 정적 getInstance() 메서드를 사용하여 클래스의 단일 인스턴스를 만들고 반환했습니다. 그러나 이 접근 방식에는 이중 확인 잠금 문제라는 잠재적인 문제가 있습니다. 이 문제는 여러 스레드가 동시에 getInstance() 메서드에 액세스 하여 잠재적으로 클래스의 여러 인스턴스를 생성할 때 발생합니다.

 

이 문제를 피하기 위해 Singleton 패턴을 사용할 수 있습니다. 리팩토링 된 코드에서 MySingleton 클래스의 정적 최종 인스턴스를 포함하는 SingletonHolder라는 비공개 내부 클래스를 만들었습니다. getInstance() 메서드가 호출되면 SingletonHolder 클래스에서 인스턴스를 반환합니다.

SingletonHolder 클래스는 getInstance()가 처음 호출될 때만 로드되고 클래스는 한 번만 로드되기 때문에 이 접근 방식은 스레드로부터 안전합니다. 따라서 MySingleton 클래스의 인스턴스를 여러 개 만들 가능성이 없습니다.

 

Before:

public class MySingleton {
  private static MySingleton instance;

  private MySingleton() {
    // Do some initialization
  }

  public static MySingleton getInstance() {
    if (instance == null) {
      instance = new MySingleton();
    }

    return instance;
  }
}

After:

public class MySingleton {
  private static class SingletonHolder {
    private static final MySingleton INSTANCE = new MySingleton();
  }

  private MySingleton() {
    // Do some initialization

7. 알고리즘 및 데이터 구조 최적화

Java 코드의 성능을 개선하는 것도 리팩토링의 중요한 부분입니다. 알고리즘과 데이터 구조를 최적화하기 위해 효율적인 알고리즘, 캐싱 및 지연 로드를 사용할 수 있습니다.

아래 Before 코드는 무차별 대입 알고리즘을 사용하여 특정 한계까지 소수를 찾습니다. 그러나 이 알고리즘은 불필요한 계산을 많이 수행하기 때문에 limit 값이 큰 경우에는 비효율적입니다.

 

리팩토링 된 코드에서는 에라토스테네스의 체 알고리즘을 사용하여 소수를 찾습니다. 이 알고리즘은 더 적은 계산을 수행하기 때문에 이전 알고리즘보다 더 효율적입니다.

에라토스테네스의 체 알고리즘은 isPrime[i]가 i가 소수인지 여부를 나타내는 부울 배열을 생성하여 작동합니다. 그런 다음 알고리즘은 첫 번째 소수(2)로 시작하여 모든 배수를 소수가 아닌 것으로 표시합니다. 그 후 다음 소수로 이동하여 한계까지 모든 소수를 찾을 때까지 프로세스를 반복합니다.

리팩토링 된 코드는 true 값으로 부울 배열 isPrime을 초기화하고 이를 사용하여 소수가 아닌 숫자를 표시합니다. 그런 다음 배열을 반복하여 소수를 찾아 primeNumbers 목록에 추가합니다.

 

Before:

public List<Integer> findPrimeNumbers(int limit) {
  List<Integer> primeNumbers = new ArrayList<>();

  for (int i = 2; i <= limit; i++) {
    boolean isPrime = true;
    for (int j = 2; j < i; j++) {
      if (i % j == 0) {
        isPrime = false;
        break;
      }
    }

    if (isPrime) {
      primeNumbers.add(i);
    }
  }

  return primeNumbers;
}

After:

public List<Integer> findPrimeNumbers(int limit) {
  List<Integer> primeNumbers = new ArrayList<>();
  boolean[] isPrime = new boolean[limit + 1];
  Arrays.fill(isPrime, true);

  for (int i = 2; i * i <= limit; i++) {
    if (isPrime[i]) {
      for (int j = i * i; j <= limit; j += i) {
        isPrime[j] = false;
      }
    }
  }

  for (int i = 2; i <= limit; i++) {
    if (isPrime[i]) {
      primeNumbers.add(i);
    }
  }

  return primeNumbers;
}

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."

728x90

댓글