본문 바로가기
Today I learned

자바로 배우는 리팩토링 입문 2

by soheemon 2019. 7. 7.

※자바로 배우는 리팩토링 입문 책을 보고 작성하였습니다.

 

소스코드에 포함되는 특정한 숫자를 매직넘버라고 한다.

예를들어 '입력가능한 문자열의 길이는 100문자 이하'라는 제한이 있을때, 문자열 길이한계인 100이라는 숫자를 매직넘버라고 할 수 있다.

 

매직넘버를 쓰면 안되는 이유

1. 무엇을 뜻하는지 바로 알기 어렵다.

매직넘버(100) 대신에 상징이되는 이름(MAX_INPUT_LENGTH)을 사용하면 의미를 알기 쉬워진다.

기호상수란 MAX_INPUT_LENGTH처럼 기호를 사용한 상수이다.

 

2.매직넘버는 수정하기 어렵다.

현재는 '입력 가능한 문자열 길이는 100문자 이하'지만 훗날 요구사항이 변해서 '200문자 이하'가 될 지도 모른다. 

//before
if (100 < input.length()) {
}

//after
public class something {
	public static final int MAX_INPUT_LENGTH = 100;
}
if(somethig.MAX_INPUT_LENGTH > input.length()){
}

리팩토링 카탈로그

이름: 매직넘버를 기호 상수로 치환

상황: 상수를 사용함

문제: 매직넘버는 의미를 알기 어려움, 매직넘버가 열어곳에 있으면 변경하기 어려움

해법: 매직 넘버를 기호 상수로 치환함

결과: 상수의 의미를 알기 쉬워짐, 기호상수의 값을 변경하면 상수를 사용하는 모든 곳이 변경됨.

방법:

- 기호상수 선언하기

(1)기호상수 선언 (2)매직 넘버를 기호상수로 치환

(3)기호 상수에 의존하는 다른 매직넘버를 찾아서 기호 상수를 사용한 표현식으로 변환 (4) 컴파일

- 테스트

(1) 모든 기호 상수 치환이 끝나면 컴파일해서 테스트 (2)가능하다면 기호 상숫값을 변경 한 후 컴파일해서 테스트

관련 항목: 분류코드를 클래스로 치환(상수가 분류 코드로 쓰인다면 클래스로 치환하는 방법이 좋을 때도 있음)

분류 코드를 상태/전략 패턴으로 치환(상수가 분류 코드로 쓰인다면 상태/전략 패턴을 사용하는 방법도 있음)

 

예제프로그램

//리팩토링 전 Robot 클래스.
public class Robot {
 private final String _name;
 public Robot(String name) {
  _name = name;
 }
public void order(int command) {
 if(command == 0){
  System.out.println(_name + " walks.");
 } else if(command == 1){
 System.out.println(_name + " stops.");
 } else if(command == 2){
 System.out.println(_name + " jumps.");
 } else{
 System.out.println("command error.");
 }
}
//리팩토링 전 main 클래스

public class Main{
 public static void main(String[] args){
 Robot robot = new Robot("Andrew");
 robot.order(0); //walk
 robot.order(1); //stop
 robot.order(2); //jump
 }
}
}

0, 1, 2의 의미를 알기 어려우니 주석을 달았다. 주석 자체는 나쁘지 않지만 주석이 없어도 되도록 기호상수를 사용하는 편이 더 낫다.

 

리팩토링 실행

1. 기호상수 선언하기

(1) 기호 상수 선언

자바에서 기호 상수를 만드는 방법은 두가지가 있다.

- public static final 클래스 필드 사용하기

- enum 사용하기.

public static final 키워드 사용 목적

- public은 클래스 외부에서도 참조할 수 있음. ( 클래스 안에서만 사용해야 할 경우 private를 사용한다.)

- static은 클래스 필드로 만듦

- final은 잘못해서 할당하지 않도록 함.

 

명령어를 나타내는 기호 상수를 Robot 클래스 안에 선언하면 다음과 같다.

public static final int COMMAND_WALK = 0;
public static final int COMMAND_STOP = 1;
public static final int COMMAND_JUMP = 2;

(2) 매직 넘버를 기호 상수로 치환

//매직넘버를 기호 상수로 치환
public void order(int command) {
// if(command == 0){
 if(command == COMMAND_WALK){
  System.out.println(_name + " walks.");
// } else if(command == 1){
 } else if(command == COMMAND_STOP){
 System.out.println(_name + " stops.");
 //} else if(command == 2){
  }else if(command == COMMAND_JUMP){
 System.out.println(_name + " jumps.");
 } else{
 System.out.println("command error.");
 }
}

//Main클래스에서 Robot클래스의 상수를 참조하려면 상수 앞에 클래스명을 붙여야 한다.
public class Main{
 public static void main(String[] args){
 Robot robot = new Robot("Andrew");
 robot.order(Robot.COMMAND_WALK); //walk
 robot.order(Robot.COMMAND_STOP); //stop
 robot.order(Robot.COMMAND_JUMP); //jump
 }
}
}

(3) 기호 상수에 의존하는 다른 매직 넘버를 찾아서 기호 상수를 사용한 표현식으로 변환한다.

(4) 컴파일.

 

+ 상수 의존관계

예를들어. 작업영역은 입력길이의 2배일때, 두개의 상수를 각각 매직넘버로 선언해서는 안된다. 아래와 같이 의존관계를 표현해야 한다.

public static final int MAX_INPUT_LENGTH = 100;

public static final int WORK_AREA_LENGTH = MAX_INPUT_LENGTH * 2;

 

2. 테스트

(1)모든 기호 상수 치환이 끝나면 컴파일해서 테스트. 테스트 결과는 리팩토링 하기 전과 같아야 한다.

(2) 가능하다면 기호 상숫값을 변경한 후 컴파일해서 테스트.

기호상수의 값을 다른값으로 변경한 후 테스트 하면 빠트린 곳이 없는지 확인 할 수 있다.

예를들어, 예제 프로그램 명령어 값을 0, 1, 2 가 아니라 100, 200, 300 처럼 바꿔도 리컴파일하면 문제 없이 동작해야 한다.

 

1.3 분류 코드를 클래스로 치환하기

위의 예제는 이른바 분류코드 이다. 분류코드는 사물의 종류를 표현하는 값이다.

분류코드는 타입에 문제가 있다. Robot.COMMAND_WALK처럼 기호 상수로 만든다고 해도 실제로는 0이라는 int값이다. 따라서 프로그래머가 다음과 같이 매직넘버 0을 직접 적어도 컴파일러는 아무런 경고를 출력하지 않는다.

//기호상수 대신 매직넘버를 직접 적다
robot.order(0);

이러면 기호 상수를 도입했는데도 누군가 실수할지도 모른다.

따라서 분류코드에 정수를 쓰지 말고 새 타입을 만들어 보자.

RobotCommand클래스는 로봇 명령어를 나타내는 타입이다.

 

public class RobotCommand {
 private final String _name;
 public RobotCommand(String name){
  _name = name;
 }
 public String toString(){
  return "[ RobotCommand: " + _name + "]";
 }
}

public class Robot{
 private final String _name;
 //여전히 public static final 기호상수이지만 int가 아닌 RobotCommand타입이 된다.
 public static final RobotCommand COMMAND_WALK = new RobotCommand("WALK");
 public static final RobotCommand COMMAND_STOP = new RobotCommand("STOP");
 public static final RobotCommand COMMAND_JUMP = new RobotCommand("JUMP");
 ....
 //order 메서드 생략
}

//지난번 코드와 달라진점은, COMMAND_WALK라는 상수가 int타입이였지만, 이번에는 객체형(RobotCommand)으로 변경됐다.

따라서 사용자는 매직넘버를 이용하여 프로그램을 동작 시킬 수 없다. ( 컴파일 에러가 발생한다.)

 

1.3.2 enum

자바 5부터 enum으로 기호 상수를 표현 할 수 있게 됐다.

public class Robot {
 private final String _name;
 public enum Command {
  WALK,
  STOP,
  JUMP
 };
 public Robot(String name) {
  _name = name;
 }
 public void order (Robot.Comamnd command) {
 if(command == Command.WALK){
  System.out.println(_name + "walks.");
 }
 //........생략
 }
}

public class Main{
 public static void main(String[] args){
  Robot robot = new Robot("Andrew");
  robot.order(Robot.Command.WALK);
  //............생략
 }
}

1.3.3 기호 상수가 적합하지 않은 경우

기호 상수는 편리하지만 기호 상수를 쓰지 않는게 좋을 때도 있다.

예를들어 for반복에서 배열의 길이를 나타내는 데 기호 상수를 쓰는건 적절하지 않다. length라는 필드가 있기 때문.

1.3.4 바이트 코드에 내장된 상수에 주의하기.

필드값이 컴파일 할 때 정해지는 상수라고 하자. 필드값을 변경한 후 리컴파일 하면 해당 필드값 자체는 변하겠지만, 그 값을 사용하는 다른 클래스에도 새로운 값이 전달 될지는 알 수 없다. 값을 사용하는 클래스에도

*리컴파일 했을 때 비로소 새로운 값이 넘어간다.*

 

예를들어,

//상수는 컴파일시 값이 정해진다.
public static final int COMMAND_WALK = 0;
public static final int COMMAND_STOP = 1;
public static final int COMMAND_JUMP = 2;

//상수의 값을 각각 100, 200, 300으로 변경후 *모든 소스를* 리컴파일 하면 프로그램은 정상적으로 동작한다.
//하지만 해당 상수값을 사용하는 *Main.java를 리컴파일 하지 않으면* 프로그램이 정상적으로 동작하지 않는다.
//==> main.class에서는 Robot.COMMAND_WALK값을 이전값으로 보기때문이다.

이런 사양은 <<The Java Language Specification>> JLSS3의 14.21 Unreachable Statements'에서 논의하므로 참조하다. C#은 readonly라는 키워드로 이런 문제에 대처한다고 한다.

댓글