본문 바로가기
Lecture

자바 코드 리팩토링: 생성자 대신 팩토리 메서드, 전략 패턴/상태 패턴 적용, 상속 대신 위임, 에러 예외 처리

by Renechoi 2023. 7. 7.

1. 생성자를 팩터리 메서드로 바꾸기 

 

문제점 

- new 키워드로 객체 생성시 클래스 이름이 노출됨 

- 생성되는 클래스 이름을 숨기거나 상황에 맞게 인스턴스 생성이 가능하고자 할 때 

- 컴파일 시간이 아닌 실행 시간에 생성되는 인스턴스가 결정되어야 할 때 

 

 

리팩토링 방법 

- 객체의 생성자를 private로 변경한다. 

- 외부로 열려 있는 static 메서드를 만들고 해당 메서드에서 객체를 생성하도록 한다. 

- 외부 호출시 new 키워드 사용을 막을 수 있다. 

 

 

 

Before 

다음과 같은 Customer 클래스와 main 함수에서 이를 호출하는 케이스를 리팩토링 해보자. 

 

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 getCustomerName() {
      return customerName;
   }

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

   public static void main(String[] args) {

      Customer bronzeCustomer = new Customer(Customer.BRONZE_CUSTOMER, "Tomas");
      Customer silverCustomer = new Customer(Customer.SILVER_CUSTOMER, "Alice");
      Customer goldCustomer = new Customer(Customer.GOLD_CUSTOMER, "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));

   }
}

 

 

 

 

 

After 

 

기존의 new를 통해 생성되는 부분은 아래와 같이 되어야 한다. 

 

public class Main {

   public static void main(String[] args) {

      Customer bronzeCustomer = BronzeCustomer.create("Tomas");
      Customer silverCustomer = SilverCustomer.create("Alice");
      Customer goldCustomer = GoldCustomer.create("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));

   }
}

 

create() 메서드를 통해 객체를 생성하도록 바꾼다. 

 

public class SilverCustomer extends Customer {

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

   public static SilverCustomer create(String customerName) {
      return new SilverCustomer(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 BronzeCustomer extends Customer {

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

   public static BronzeCustomer create(String customerName) {
      return new BronzeCustomer(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 GoldCustomer extends Customer {

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

   public static GoldCustomer create(String customerName) {
      return new GoldCustomer(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);
   }

}

 

 

Customer 클래스는 추상 클래스로서 객체 생성하지 못하도록 변경된다. 

public abstract class Customer {

   private String customerName;
   protected int bonusPoint;

   protected 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() + "입니다.";
   }
}

 

 

 

 

2. 전략 패턴/상태 패턴 적용

 

문제점 

- 어떤 정책 혹은 필요에 따라 여러 분기문이 생길 수 있다. 

- 케이스 별로 하는 일이 다를 때 이를 분기문으로 계속 확장하는 것은 유연하지 못하고 유지보수성을 떨어뜨린다. 

 

 

리팩토링 방법 

- 상태패턴: 각 상태를 클래스로 만들고 수행하는 로직을 각 클래스에 작성한다. 

- 전략패턴: 전략마다 클래스로 나누고 각 클래스에서 수행하는 로직을 구현한다. 

 

-> 두 패턴 모두 상속과 오버라이딩을 활용한 패턴이다.

-> 상태 패턴은 클래스의 상태(주로 변수)에 관련하여 여러 가지 수행이 있는 경우

-> 전략 패턴은 알고리즘이 여러개 존재하는 경우

 

 

 

 

Before 

 

플레이어의 레벨(=상태)에 따라서 각각 다른 로직을 수행하는 경우를 생각해보자. 레벨에 따라 차등되는 로직을 분기하면 다음과 같이 작성할 수 있다. 

 

import lombok.Getter;

@Getter
public class Player {

   public static final int BEGINNER_LEVEL = 1;
   public static final int ADVANCED_LEVEL = 2;
   public static final int SUPER_LEVEL = 3;
   private int level;

   public Player() {
      level = BEGINNER_LEVEL;
      showLevelMessage();
   }
   

   public void upgradeLevel() {
      if (level == BEGINNER_LEVEL) {
         level = ADVANCED_LEVEL;
      } else if (level == ADVANCED_LEVEL) {
         level = SUPER_LEVEL;
      } else {
         System.out.println("not support level");
      }
      showLevelMessage();
   }

   public void play(int count) {
      run();
      for (int i = 0; i < count; i++) {
         jump();
      }
      turn();
   }

   public void run() {
      if (level == BEGINNER_LEVEL) {
         System.out.println("천천히 달립니다..");
      } else if (level == ADVANCED_LEVEL) {
         System.out.println("빨리 달립니다");
      } else if (level == SUPER_LEVEL) {
         System.out.println("엄청 빨리 달립니다.");
      } else {
         System.out.println("not support level");
      }

   }

   public void jump() {
      if (level == BEGINNER_LEVEL) {
         System.out.println("Jump 할 줄 모르지롱.");
      } else if (level == ADVANCED_LEVEL) {
         System.out.println("높이 jump 합니다");
      } else if (level == SUPER_LEVEL) {
         System.out.println("엄청 높게 jump합니다.");
      } else {
         System.out.println("not support level");
      }

   }

   public void turn() {

      if (level == BEGINNER_LEVEL) {
         System.out.println("Turn 할 줄 모르지롱.");
      } else if (level == ADVANCED_LEVEL) {
         System.out.println("Turn 할 줄 모르지롱.");
      } else if (level == SUPER_LEVEL) {
         System.out.println("한 바퀴 돕니다.");
      } else {
         System.out.println("not support level");
      }
   }

   public void showLevelMessage() {

      if (level == BEGINNER_LEVEL) {
         System.out.println("***** 초보자 레벨 입니다. *****");
      } else if (level == ADVANCED_LEVEL) {
         System.out.println("***** 중급자 레벨 입니다. *****");
      } else if (level == SUPER_LEVEL) {
         System.out.println("***** 고급자 레벨 입니다. *****");
      } else {
         System.out.println("not support level");
      }
   }
}
public class MainBoard {

   public static void main(String[] args) {

      Player player = new Player();
      player.play(1);
      player.upgradeLevel();
      player.play(2);
      player.upgradeLevel();
      player.play(3);

   }
}

 

 

이와 같은 코드에서 마스터 레벨을 추가한다고 하면 어떨까? 

 

먼저 맨 위 상수 선언에서 prviate static final int MASTER_LEVEL = 4; 를 작성하고 관련된 로직에서 모두 마스터를 추가해주어야 한다. 

 

이를 상태 패턴을 이용해 변경해보자. 

 

 

 

After 

 

먼저 레벨의 추상클래스를 선언한다. 

 

public abstract class PlayerLevel {

   public abstract void run();

   public abstract void jump();

   public abstract void turn();

   public abstract void showLevelMessage();

   final public void go(int count) {
      run();
      for (int i = 0; i < count; i++) {
         jump();
      }
      turn();
   }
}

 

레벨이 할 수 있는 것들을 추상 메서드로서 정의한다. 이를 플레이어들이 상속하여 구체적인 레벨에 따라 다른 행동을 할 수 있다. 

 

예를 들어 AdvancedLevel을 보면 다음과 같다. 

 

public class AdvancedLevel extends PlayerLevel {

   private static AdvancedLevel instance;

   private AdvancedLevel() {
   }

   public static AdvancedLevel getInstance() {
      return instance == null ? new AdvancedLevel() : instance;
   }

   @Override
   public void run() {
      System.out.println("빨리 달립니다");
   }

   @Override
   public void jump() {
      System.out.println("높이 jump 합니다");
   }

   @Override
   public void turn() {
      System.out.println("Turn 할 줄 모르지롱.");
   }

   @Override
   public void showLevelMessage() {
      System.out.println("***** 중급자 레벨 입니다. *****");
   }
}

 

 

이때 Level은 계속해서 생성될 필요가 없으므로 싱글톤으로 작성한다. 상황에 따라 싱글톤으로 하면 안되는 경우도 있을 수 있다. 

 

다른 레벨도 만들어주자. 

 

public class BeginnerLevel extends PlayerLevel {

   private static BeginnerLevel instance;

   public static BeginnerLevel getInstance() {
      return instance == null ? new BeginnerLevel() : instance;
   }

   private BeginnerLevel() {
   }

   @Override
   public void run() {
      System.out.println("천천히 달립니다..");
   }

   @Override
   public void jump() {
      System.out.println("Jump 할 줄 모르지롱.");
   }

   @Override
   public void turn() {
      System.out.println("Turn 할 줄 모르지롱.");
   }

   @Override
   public void showLevelMessage() {
      System.out.println("***** 초보자 레벨 입니다. *****");
   }

}

 

public class SuperLevel extends PlayerLevel {

   private static SuperLevel instance;

   public static SuperLevel getInstance() {
      return instance == null ? new SuperLevel() : instance;
   }

   private SuperLevel() {
   }

   @Override
   public void run() {
      System.out.println("엄청 빨리 달립니다.");

   }

   @Override
   public void jump() {
      System.out.println("엄청 높게 jump합니다.");
   }

   @Override
   public void turn() {
      System.out.println("한 바퀴 돕니다.");
   }

   @Override
   public void showLevelMessage() {
      System.out.println("***** 고급자 레벨 입니다. *****");
   }

}

 

플레이어는 레벨을 갖도록 구현한다. 

 

public class Player {
   
   private PlayerLevel level;
   
   public Player() {
      level= BeginnerLevel.getInstance();
      level.showLevelMessage();
   }

   public PlayerLevel getLevel() {
      return level;
   }

   public void upgradeLevel(PlayerLevel level) {
      this.level = level;
      level.showLevelMessage();
   }
   
   public void play(int count){
      level.go(count);
   }
}

 

 

어떻게 레벨을 설정해주고 사용할 수 있을까? 다음과 같이 작성한다. 

 

public class MainBoard {

   public static void main(String[] args) {

      Player player = new Player();
      player.play(1);
      AdvancedLevel aLevel = AdvancedLevel.getInstance();
      player.upgradeLevel(aLevel);
      player.play(2);
      SuperLevel sLevel = SuperLevel.getInstance();
      player.upgradeLevel(sLevel);
      player.play(3);
      
   }
}

 

 

 

 

 

3. 상속 대신 위임

 

문제점 

- 코드를 재사용한다는 관점에서는 상속보다 위임의 개념이 더 적절하다. 

   -> 상속을 사용하면 클래스 종속성이 더 타이트해진다. 

   -> 상속은 반드시 써야할 때만 쓰자. (좀 더 일반적인 클래스를 보다 더 구체적으로(확장하는) 쓸 필요성이 있을 때)

- Is-A 관계와 Has-A 관계에 대해 생각해보자. 

- Is-A 관계

 

  • Is-A 관계클래스간의 상속은 종속성이 강하게 되므로, 꼭 필요한 경우 사용하는 것이 좋음
  • 상속은 코드의 재사용을 위한 방법이 아님
  • 일반적인 클래스와 구체적인 클래스의 관계

- Has-A 관계 

  • Has-A 관계사용할 클래스를 포함하여 그 기능을 해당 클래스로 위임한다.
  • 클래스간의 종속성이 약함
  • 이미 잘 만들어진 라이브러리나 클래스를 재사용하는 방법

 

 

-> 코드 재사용시엔 합성을 쓴다. 

 

 

 

리팩토링 방법 

- Is-A 관계가 아닌 클래스간의 상속은 Has-A로 리펙토링 한다.

 

 

 

Before 

 

 

학생 클래스가 과목 클래스를 상속하는 경우를 생각해보자. 과목 클래스 내의 score를 사용하고 싶기 때문에 extends를 하는 경우이다. 

 

그런데 Is-A 관계가 아니기 때문에 Subject를 상속하는 의미가 없다. 

 

-> 단순히 코드를 많이 쓸 것 같다고 해서 상속을 하면 안 된다. 

 

@Getter
@Setter
@NoArgsConstructor
public class Subject {

   String subjectName;
   int subjectCode;
   int score;
   
   public Subject(String subjectName, int subjectCode) {
      this.subjectName = subjectName;
      this.subjectCode = subjectCode;
   }

   public String toString() {
      return subjectCode + ":" + subjectName;
   }
}

 

public class Student extends Subject {

   String studentName;
   String studentId;
   Subject majorSubject;

   public Student(String studentName, String studentId, String subjectName, int subjectCode) {
      majorSubject = new Subject(subjectName, subjectCode);

      this.studentName = studentName;
      this.studentId = studentId;
   }

   public void addSubject(String subjectName, int subjectCode) {
      Subject subject = new Subject(subjectName, subjectCode);
      subjectList.add(subject);
   }

   public void setSubjectScore(int subjectCode, int score) {
      for (Subject subject : subjectList) {
         if (subject.getSubjectCode() == subjectCode) {
            subject.setScore(score);
            return;
         }
      }
      System.out.println("no subject code error");
   }

   public void getReport() {
      int total = majorSubject.score;

      System.out.println(studentName + "님의 전공과목은 " + majorSubject.getSubjectName() + "입니다.");
      System.out.println(majorSubject.getSubjectName() + ":" + majorSubject.getScore());

      for (Subject subject : subjectList) {
         System.out.println(subject.getSubjectName() + ":" + subject.getScore());
         total += subject.getScore();
      }

      System.out.println("Total Score : " + total);
   }
}

 

 

 

 

After 

 

상속관계를 끊고 Student 클래스에서 Subject 클래스를 포함하도록 변경하자. 

 

public class Student {

   String studentName;
   String studentId;
   Subject majorSubject;

   ArrayList<Subject> subjectList = new ArrayList<>();
   
   // ..

 

 

4. 에러 예외 처리

 

문제점 

- 정상적인 처리와 에러 처리의 내용이 코드에 같이 혼재하는 경우가 있다. 

   -> 일관성이 없음 

   -> 놓치기 쉬움 
- 에러 코드 상황이 점점 추가되면 어떨까? 

 

리팩토링 방법 

- 예외 상황을 Exeption 클래스로 만들어 관리하자.

 

 

Before 

public class Password {

		private String password;
		
		public String getPassword(){
			return password;
		}
		
		public void setPassword(String password) {
			
			if(password == null){
				 System.out.println( "비밀번호는 null 일 수 없습니다");
				 return;
			}
			else if( password.length() < 5){
				System.out.println( "비밀번호는 5자 이상이어야 합니다.");
				return;
			}
			else if (password.matches("[a-zA-Z]+")){
				System.out.println("비밀번호는 숫자나 특수문자를 포함해야 합니다.");
				return;
			}
			else {
				System.out.println("오류 없음");
			}
			
			this.password = password;
		}
}

 

 

 

After 


public class Password {

   private String password;
      
   public String getPassword() {
      return password;
   }

   public void setPassword(String password) throws PasswordException {
      if (password == null) {
         throw new PasswordException("비밀번호는 null 일 수 없습니다");
      } else if (password.length() < 5) {
         throw new PasswordException("비밀번호는 5자 이상이어야 합니다.");
      } else if (password.matches("[a-zA-Z]+")) {
         throw new PasswordException("비밀번호는 숫자나 특수문자를 포함해야 합니다.");
      }
      this.password = password;
   }
}

 

 


참고자료

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

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

 

 

반응형