본문 바로가기
Lecture

자바 코드 리팩토링: 매직넘버, 제어 플래그, 널 문제, 분류 코드 문제, 분기문 문제

by Renechoi 2023. 7. 6.

1. 매직넘버를 상수로 바꾸기 

 

문제점 

- 의미를 알기 어렵다.

- 100이라는 숫자를 쓴다고 해보자. 바뀌는 경우 어떻게 해야할까? 하나하나를 바꿔줘야 한다.

 -> 수정하기 어렵다.

 

public void doSomething(int command){
   if (command==0){
      System.out.println("walk");
   } else if (command == 1) {
      System.out.println("stop");
   } else if (command ==2) {
      System.out.println("jump");
   }
}
Robot robot = new Robot("K");
robot.doSomething(0);

 

로봇 객체가 doSomething 메서드를 수행하나고 할 때 명령어를 입력을 하는데 0이라는 매직넘버에 대해서 어떤 의미를 부여하기가 힘들다. 

 

작성자도 쉽게 까먹을 확률이 높고, 다른 사람이 보아도 무슨 의미인지 모른다. 

 

 

 

 

리팩토링 방법 

- 상수로 치환하여 리팩토링한다. 

 

 

Before 

public class Robot {
   private String name;

   public Robot(String name) {
      this.name = name;
   }

   public void doSomething(int command){
      if (command==0){
         System.out.println("walk");
      } else if (command == 1) {
         System.out.println("stop");
      } else if (command ==2) {
         System.out.println("jump");
      }
   }
}
public class MagicNumberMain {
   public static void main(String[] args) {
      Robot robot = new Robot("K");
      robot.doSomething(0);
   }
}

 

 

 

After 

 

public class Robot {

   public static final int COMMAND_WALK = 0;
   public static final int COMMAND_STOP = 1;
   public static final int COMMAND_JUMP = 2;
   public enum Command{
      WALK, STOP, JUMP
   }
   private String name;

   public Robot(String name) {
      this.name = name;
   }

   public void doSomething(int command){
      if (command==COMMAND_WALK){
         System.out.println("walk");
      } else if (command == COMMAND_STOP) {
         System.out.println("stop");
      } else if (command ==COMMAND_JUMP) {
         System.out.println("jump");
      }
   }

   public void doSomething(Command command){
      if (command==Command.WALK){
         System.out.println("walk");
      } else if (command == Command.STOP) {
         System.out.println("stop");
      } else if (command ==Command.JUMP) {
         System.out.println("jump");
      }
   }
}

 

public class MagicNumberMain {
   public static void main(String[] args) {
      Robot robot = new Robot("K");
      robot.doSomething(Robot.COMMAND_JUMP);
      robot.doSomething(Robot.Command.JUMP);
   }
}

 

 

 

2. 제어를 위한 플래그 삭제 

 

 

문제점 

예를 들어 다음과 같은 코드를 보자 

 

for (int i =0; i<data.length; i++) {
   if (data[i] == number) {
      flag = true;
   }
}

- 플래그를 잘못 사용하면 처리 흐름을 이해하기 어렵다.

- 크게 의미 없는 이름의 boolean일 경우 이해하기 어려운 코드를 만든다.

 

 

 

리팩토링 방법 

- 제어 플래그 삭제

- 제어 플래그의 적절한 네이밍을 한다. 

- 제어 플래그 대신 break, continue, return 등을 사용할 수 있다면 사용한다. 

 

 

 

Before

public static boolean find(int[] data, int number) {
   
   boolean flag = false;
   
   for(int i=0; i<data.length; i++) {
      if(data[i] == number) {
         flag = true;
      }
   }  
   
   return flag;
}

찾으면 끝인데 플래그를 사용해서 코드 복잡도를 높이고 있다. 

 

After 

public static boolean find(int[] data, int number) {
   
   boolean found = false;
   
   for(int i=0; i<data.length; i++) {
      if(data[i] == number) {
         found = true;
         //break;
         return found;
      }
   }  
   
   return found;
}

 

 

Before

 

또 다른 예시를 보자. 

 

flag를 비롯해서 변수명이 무엇을 하는지 알 수가 없도록 짜여있다. 

 

이 코드의 의도는 두 개의 페어 key, value를 읽어서 key와 value별로 map에 넣는 것이다. 

여기서 flag의 역할은 더 이상 읽을 라인이 있느냐를 체크한다. 또 다른 flag2는 assignment 전후로 나누기 위해서 존재한다. 

 

즉, parsing을 하는 코드이다. 

 

public class SimpleDataBase {

   private Map<String, String> map = new HashMap<>();

   public SimpleDataBase(Reader reader) throws IOException {
      BufferedReader br = new BufferedReader(reader);

      boolean flag = false;
      String temp;

      while (!flag) {

         temp = br.readLine();
         if (temp == null) {
            flag = true;

         } else {
            boolean flag2 = true;

            StringBuffer sb1 = new StringBuffer();
            StringBuffer sb2 = new StringBuffer();

            for (int i = 0; i < temp.length(); i++) {
               char temp2 = temp.charAt(i);

               if (flag2) {
                  if (temp2 == '=') {
                     flag2 = false;
                  } else {
                     sb1.append(temp2);
                  }
               } else {
                  sb2.append(temp2);
               }
            }

            String ss1 = sb1.toString();
            String ss2 = sb2.toString();
            map.put(ss1, ss2);
         }
      }
   }

   public Iterator<String> iterator() {
      return map.keySet().iterator();
   }

   public String getValue(String key) {
      return map.get(key);
   }

   public String toString() {
      return map.toString();
   }
}

 

After 

 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class SimpleDataBase2 {

   private Map<String, String> map = new HashMap<>();

   public SimpleDataBase2(Reader reader) throws IOException {
      BufferedReader br = new BufferedReader(reader);

      boolean reading = false;
      String line;

      while (!reading) {

         line = br.readLine();
         if (line == null) {
            break;
         }

         int index = line.indexOf('=');

         if (index > 0) {
            String key = line.substring(0, index);
            String value = line.substring(index + 1, line.length());

            map.put(key, value);
         }

      }
   }

   public Iterator<String> iterator() {
      return map.keySet().iterator();
   }

   public String getValue(String key) {
      return map.get(key);
   }

   public String toString() {
      return map.toString();
   }
}

 

 

 

3. 널 객체 사용

 

문제점 

- null을 계속 확인하는 코드가 많은 경우 좋지 않다. 

- 다음과 같은 예시를 보자. 

    public String toString() {
		
		String result = "[ Person : ";
		
		result += " name =";
		if(name == null) {
			result += "\"(none)\"";
		}
		else {
			result += name;
		}
		
		result += " mail =";
		if(mail == null) {
			result += "\"(none)\"";
		}
		else {
			result += mail;
		}
		
		result += " ]";
		
		return result;
	}

 

toString() 메서드에서 name과 mail 변수의 null 여부를 확인하고, 해당 값을 출력하는 부분에서 조건문을 사용하고 있다. 문제는 객체의 속성이 추가되면 계속적인 null 확인 코드를 작성해야 해서 번거로움을 야기한다는 것이다. .

 

 

 

리팩토링 방법 

- 빈 객체를 만들어 사용하여 null 확인 코드를 줄여주는 것이 좋다. 

- 널 객체 클래스를 작성한다.isNull() 메서드 작성 : 기존 클래스는 false를 널 클래스는 true를 반환한다.

- 널 객체 클래스를 재정의하여 조건 판단하기

 

 

Before 

 

public class Label {

   private String label;
   
   public Label(String label) {
      this.label = label;
   }
   
   public void display() {
      System.out.println("display :" + label);
   }
   
   public String toString() {
      return "\"" + label + "\"";
   }
}
public class Person {

   private Label name;
   private Label mail;
   
   public Person(Label name, Label mail) {
      this.name = name;
      this.mail = mail;
   }
   
   public Person(Label name) {
      this(name, null);
   }
   
   public void display() {
      if(name != null) {
         name.display();
      }
      
      if(mail !=null) {
         mail.display();
      }
   }
   
   public String toString() {
      
      String result = "[ Person : ";
      
      result += " name =";
      if(name == null) {
         result += "\"(none)\"";
      }
      else {
         result += name;
      }
      
      result += " mail =";
      if(mail == null) {
         result += "\"(none)\"";
      }
      else {
         result += mail;
      }
      
      result += " ]";
      
      return result;
      
      
   }
}

 

null이 contstructor 파라미터로 들어오는 경우가 있다. 이때 null을 그냥 초기화해준다. 

 

원하는 로직은 null 인 경우에는 불리지 않도록 하는 것이다. 

 

예를 들어 다음과 같이 사용한다고 해보자. 

 

public static void main(String[] args) {

   Person people[] = {
         new Person(new Label("Alice"), new Label("alice@aaa.com")),
         new Person(new Label("James"), new Label("james@aaa.com")),
         new Person(new Label("Tomas")) 
   };
   
   for(Person person : people) {
      System.out.println(person.toString());
      person.display();
      System.out.println();
   }
}

 

 

 

After 

 

 

먼저 null객체를 만든다. 

 

public class NullLabel extends Label{

   public NullLabel() {
      super("(none)");
   }

   @Override
   public boolean isNull() {
      return true;
   }

   @Override
   public void display() {

   }
}

 

이름이 없는 경우 슈퍼를 호출해 "none"으로 초기화한다. 즉, 이 객체는 isNull 판단을 통해 하드코딩으로 null을 처리하는 행위를 null 객체에게 세팅을 해주어 유연하게 사용할 수 있다는 데 의의가 있다. 

 

아래와 같이 isNull() 메서드를 Label에 추가한다. 

 

public class Label {

   private String label;
   
   public Label(String label) {
      this.label = label;
   }
   
   public void display() {
      System.out.println("display :" + label);
   }
   
   public String toString() {
      return "\"" + label + "\"";
   }
   
   public boolean isNull() {
      return false;
   }
}

 

이제 Person 객체를 수정해보자. 

 

public class Person {

   private Label name;
   private Label mail;

   public Person(Label name, Label mail) {
      this.name = name;
      this.mail = mail;
   }

   public Person(Label name) {
      this(name, new NullLabel());
   }

   public void display() {
      name.display();
      mail.display();
   }

   public String toString() {
      return "[ Person : name = " + name + " mail= " + mail + " ]";
   }
}

 

Label 값이 제공되지 않는 경우 이전에는 null로 초기화했지만 이제는 null 객체로 초기화할 수 있게 됐다. 

 

이때 display() 함수를 봐보자. 이전에는 null 인지를 if문으로 체크해야 했다면 이제는 그냥 호출해도 된다. 

 

public void display() {
   name.display();
   mail.display();
}

 

null 객체에서 display() 함수를 구현해주었기 때문이다. 

 

@Override
public void display() {
}

 

4. 분류 코드를 클래스로 만들기 

 

문제점 

- 메뉴나 카테고리 등을 만들 때 분류 코드를 쓰게 된다: BOOK:0, FOOD:1, CLOTH:2 등등...

 

다음과 같은 예를 보자 

 

public static final int BOOK = 0;
public static final int FOOD =1;
public static final int CLOTH =2; 

new Category(100); //<= 사용하지 않는 분류 코드가 들어갈 수 있다.

 

상수로 사용할 때 하드코딩 하는 것보다는 낫지만 여전히 문제가 있다. 

- 예외의 값이 사용될 수 있다. 

- 컴파일시에 구별 불가 

- 다른 코드와 혼란 가능성 

 

 

리팩토링 방법 

- 분류 코드를 클래스로 치환한다. 

- 새로운 분류 코드 클래스를 작성하여 기존 코드에서 사용

 

 

Before 

 

import lombok.Getter;

@Getter
public class Item {
   public static final int TYPECODE_BOOK = 0;
   public static final int TYPECODE_DVD = 1;
   public static final int TYPECODE_SOFTWARE = 2;

   private final int typeCode;
   private final String title;
   private final int price;

   public Item(int typeCode, String title, int price) {
      this.typeCode = typeCode;
      this.title = title;
      this.price = price;
   }

   public String toString() {
      return "[" + getTypeCode() + "," + getTitle() + "," + getPrice() + "]";
   }
}

 

Type에 대해 상수로 선언하여 숫자로 관리하고 있다. 

 

 

After 

 

 

선언한 상수 타입을 클래스로 만들어서 관리한다. 즉, 이경우 ItemType이라는 클래스가 된다. 

 

@Data
@AllArgsConstructor
public class ItemType {

   private int typeCode;

   public static final ItemType BOOK = new ItemType(0);
   public static final ItemType DVD = new ItemType(1);
   public static final ItemType SOFTWARE = new ItemType(2);
}

 

Item은 다음과 같이 변경한다. 

 

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class Item {
   public static final int TYPECODE_BOOK = ItemType.BOOK.getTypeCode();
   public static final int TYPECODE_DVD = ItemType.DVD.getTypeCode();
   public static final int TYPECODE_SOFTWARE = ItemType.SOFTWARE.getTypeCode();
   
   private final ItemType itemType;
   private final String title;
   private final int price;
   
   public String toString() {
      return "[" + itemType.getTypeCode() + "," + getTitle() + "," + getPrice() + "]";
   }
}

 

 

기존에는 분류 코드를 상수로 정의하는 방식을 사용하고 있었다. (예: TYPECODE_BOOK)


After 코드는 ItemType이라는 클래스를 생성하여 분류 코드를 클래스로 관리하도록 변경한다. 

 

ItemType 클래스는 분류 코드를 나타내는 typeCode 속성을 가지며, BOOK, DVD, SOFTWARE와 같은 정적 필드로 인스턴스를 미리 생성하여 사용한다.

 

Item은 따라서 int로 설정된 type을 더 이상 필드로 참조하지 않고 대신 ItemType을 참조한다. 

 

이제 Item 생성이 될 때 무의미한 100 같은 숫자가 제공될 수 없다. 기존에 설정된 Type만으로 생성되어야 하도록 변경되었다. 

 

 

 

 

5. if-else, switch 분기문을 클래스로 변경하기 

 

 

문제점 

- 지속적인 분기문으로 인한 반복적인 문장이 발생한다.

- 분류 코드의 내용이 추가되는 경우 여러 메서드를 수정해야 한다. 

 

 

리팩토링 방법 

- 분류 코드에 따른 기능을 하위 클래스로 이전하여 오버라이딩 해보자. 

 

 

 

Before

고객들이 있고 등급에 따른 차등이 있는 상황에서 타입에 따라 다른 로직을 수행해야 하는 경우이다. 

 

 

@Getter
public class Customer {

   private int customerType;
   private String customerName;
   private String customerGrade;
   private int bonusPoint;

   public static final int BRONZE_CUSTOMER = 0;
   public static final int SILVER_CUSTOMER = 1;
   public static final int GOLD_CUSTOMER = 2;

   Customer(int customerType, String customerName) {
      this.customerType = customerType;
      this.customerName = customerName;
   }

   public String getCustomerGrade() {

      switch (customerType) {
         case BRONZE_CUSTOMER:
            return "BRONZE";
         case SILVER_CUSTOMER:
            return "SILVER";
         case GOLD_CUSTOMER:
            return "GOLD";
         default:
            return null;
      }
   }

   public int calcPrice(int price) {
      switch (customerType) {
         case BRONZE_CUSTOMER:
            return price;
         case SILVER_CUSTOMER:
            return price - (int)(price * 0.05);
         case GOLD_CUSTOMER:
            return price - (int)(price * 0.1);
         default:
            return price;

      }
   }

   public int calcBonusPoint(int price) {
      switch (customerType) {
         case BRONZE_CUSTOMER:
            return bonusPoint += (price * 0.01);
         case SILVER_CUSTOMER:
            return bonusPoint += (price * 0.05);
         case GOLD_CUSTOMER:
            return bonusPoint += (price * 0.1);
         default:
            return price;

      }
   }
   
   public String toString() {
      return customerName + "님의 멤버십 등급은 " + getCustomerGrade() + "입니다.";
   }
}

 

 

고객 등급에 따라 메서드들이 분기되고 각자 다른 로직을 수행한다. 

 

 

 

After

 

Customer를 추상클래스로서 정의한다.  이름을 갖고 보너스 포인트를 갖는다. 

 

public abstract class Customer {

   private String customerName;
   protected int bonusPoint;

   Customer(String customerName) {
      this.customerName = customerName;
   }

   public abstract String getCustomerGrade();

   public abstract int calcPrice(int price);

   public abstract int calcBonusPoint(int price);

   public String getCustomerName() {
      return customerName;
   }

   public String toString() {
      return customerName + "님의 멤버십 등급은 " + getCustomerGrade() + "입니다.";
   }
}

 

각각의 등급에 따른 Customer를 아래와 같이 구현한다. 

 

public class BronzeCustomer extends Customer {

   BronzeCustomer(String customerName) {
      super(customerName);
   }

   public int calcPrice(int price) {
      return price;
   }

   public String getCustomerGrade() {
      return "BRONZE";
   }

   public int calcBonusPoint(int price) {
      return bonusPoint += (price * 0.01);
   }

}

 

public class SilverCustomer extends Customer {

   SilverCustomer(String customerName) {
      super(customerName);
   }

   @Override
   public String getCustomerGrade() {
      return "SILVER";
   }

   @Override
   public int calcPrice(int price) {
      return price - (int)(price * 0.05);
   }

   @Override
   public int calcBonusPoint(int price) {
      return bonusPoint += (price * 0.05);
   }

}

 

public class GoldCustomer extends Customer {

   GoldCustomer(String customerName) {
      super(customerName);
   }

   @Override
   public String getCustomerGrade() {
      return "GOLD";
   }

   @Override
   public int calcPrice(int price) {
      return price - (int)(price * 0.1);
   }

   @Override
   public int calcBonusPoint(int price) {
      return bonusPoint += (price * 0.1);
   }

}

 

 

 

 

기존 코드에서 if-else나 switch 분기문을 사용하는 방식에서는 등급이 하나 추가되면 Case를 또 늘려주어야 했는데, 모든 코드를 고쳐야 된다. 

 

리팩토링을 통해 Customer 클래스를 추상 클래스로 변경하고, 각 등급에 해당하는 하위 클래스를 만든다. 각 하위 클래스에서는 고객 등급에 따른 메서드를 구현하고, 필요한 로직을 오버라이딩하여 처리하도록 한다. 

 

이를 사용하는 예시는 다음과 같다. 

 

public static void main(String[] args) {

   Customer bronzeCustomer = new BronzeCustomer("Tomas");
   Customer silverCustomer = new SilverCustomer("Alice");
   Customer goldCustomer = new GoldCustomer("Edward");

   int price = 10000;

   System.out.println(bronzeCustomer);
   System.out.println(bronzeCustomer.getCustomerName() + ": price :" + bronzeCustomer.calcPrice(price)
      + ": point :" + bronzeCustomer.calcBonusPoint(price));
   System.out.println(silverCustomer);
   System.out.println(silverCustomer.getCustomerName() + ": price :" + silverCustomer.calcPrice(price)
      + ": point :" + silverCustomer.calcBonusPoint(price));

   System.out.println(goldCustomer);
   System.out.println(goldCustomer.getCustomerName() + ": price :" + goldCustomer.calcPrice(price)
      + ": point :" + goldCustomer.calcBonusPoint(price));
}

 

 


참고자료

- 패스트 캠퍼스 (박은종의 객체지향 설계를 위한 디자인패턴 with 자바)

- 리팩터링 2Edition(지은이: 마틴 파울러, 출판: 한빛 미디어) 

 

 

반응형