ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Refactoring ① : 첫번째 예제
    JAVA/Java 2021. 12. 17. 11:20

     

     

     

    https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=339765

     

    Refactoring

    리팩토링은 소프트웨어의 외부 기능을 변경하지 않으면서 내부 구조를 바꾸는 기술이다. 리팩토링을 사용하면 나쁜 디자인의 코드를 취해서, 외부 기능을 변경하지 않고, 좋은 디자인의 코드로

    www.aladin.co.kr

     

     

     

    기존의 코드
    public class Movie {
       public static final int CHILDRENS = 2;
       public static final int REGULAR = 0;
       public static final int NEW_RELEASE = 1;
       private String _title;
       private int _priceCode;
       
       public Movie(String title, int priceCode) {
         _title = title;
         _priceCode = priceCode;
       }
       
       public int getPriceCode() {
         return _priceCode;
       }
       public void setPriceCode(int arg) {
         _priceCode = arg;
       }
       public String getTitle (){
         return _title;
       };
     }
    class Rental {
       private Movie _movie;
       private int _daysRented;
       
       public Rental(Movie movie, int daysRented) {
         _movie = movie;
         _daysRented = daysRented;
       }
       
       public int getDaysRented() {
         return _daysRented;
       }
       
       public Movie getMovie() {
         return _movie;
       }
     }
     class Customer {
       private String _name;
       private Vector _rentals = new Vector();
       
       public Customer (String name){
         _name = name;
       };
       
       public void addRental(Rental arg) {
         _rentals.addElement(arg);
       }
       
       public String getName (){
         return _name;
       }
       
       public String statement() {
         double totalAmount = 0;
         int frequentRenterPoints = 0;
         Enumeration rentals = _rentals.elements();
         String result = "Rental Record for " + getName() + "\n";
         
         while (rentals.hasMoreElements()) {
           double thisAmount = 0;
           Rental each = (Rental) rentals.nextElement();
           
           //각 영화에 대한 요금 결정
           switch (each.getMovie().getPriceCode()) {
             case Movie.REGULAR:
               thisAmount += 2;
               if (each.getDaysRented() > 2) 
               	thisAmount += (each.getDaysRented() - 2) * 1.5;
               break;
             case Movie.NEW_RELEASE:
               thisAmount += each.getDaysRented() * 3;
               break;
             case Movie.CHILDRENS:
               thisAmount += 1.5;
               if (each.getDaysRented() > 3)
               	thisAmount += (each.getDaysRented() - 3) * 1.5;
               break;
           }
         
         // 포인트 추가
         frequentRenterPoints ++;
         
         // 최신 영화를 이틀 이상 대여하는 경우 추가 포인트 제공
         if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) frequentRenterPoints ++;
         
         //이 대여에 대한 요금 계산 결과 표시
         result += "\t" + each.getMovie().getTitle()+ "\t" + String.valueOf(thisAmount) + "\n";
         totalAmount += thisAmount;
         }
         
         //풋터 추가
         result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
         result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
         return result;
       }
     }

     

     

     

    statement메소드의 분해 및 재분배 

    먼저 switch 구문을 분리 해서 작성해줄 것이다. 

    ① switch 구문을 customer 클래스 안의 새로운 메소드 (private double amountFor(Rental each){})로 빼내어 줄 수 있다.

    ② 그런데 amountFor메소드 안에서 다루는 정보가 모두 Rental클래스의 정보라는 것을 알 수 있다. Rental클래스로 옮겨서 역할에 따라서 분리해주자. 

    public class Rental {
    
        private Movie _movie;
        private int _daysRented;
    
        public Rental(Movie movie, int daysRented) {
            _movie = movie;
            _daysRented = daysRented;
        }
    
        public int getDaysRented() {
            return _daysRented;
        }
    
        public Movie getMovie() {
            return _movie;
        }
    
        public double getCharge() {
            double amount = 0;
    
            switch (getMovie().getPriceCode()) {
                case Movie.REGULAR:
                    amount += 2;
                    if (getDaysRented() > 2) {
                        amount += (getDaysRented() - 2) * 1.5;
                    }
                    break;
                case Movie.NEW_RELEASE:
                    amount += getDaysRented() * 3;
                    break;
                case Movie.CHILDRENS:
                    amount += 1.5;
                    if (this._daysRented > 3) {
                        amount += (getDaysRented() - 3) * 1.5;
                    }
                    break;
            }
            return amount;
        }
    }
     public String statement() {
            double totalAmount = 0;
            int frequentRenterPoints = 0;
            Enumeration rentals = _rentals.elements();
            String result = "Rental Record for " + getName() + "\n";
    
            while (rentals.hasMoreElements()) {
                Rental each = (Rental) rentals.nextElement();
    
                //각 영화에 대한 요금 결정 (단 한줄로 표현될 수 있다)
                double thisAmount = each.getCharge();

    ③ thisAmount 값은 대여에 대한 요금 계산 결과 표시에서만 사용되며 getCharge()로 한번 정해지면 변경되지 않으므로 thisAmount를 제거하고 each.getCharge()로 표현해주자.

        public String statement() {
            double totalAmount = 0;
            int frequentRenterPoints = 0;
            Enumeration rentals = _rentals.elements();
            String result = "Rental Record for " + getName() + "\n";
    
            while (rentals.hasMoreElements()) {
                Rental each = (Rental) rentals.nextElement();
    
                // 포인트 추가
                frequentRenterPoints++;
    
                // 최신 영화를 이틀 이상 대여하는 경우 추가 포인트 제공
                if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) {
                    frequentRenterPoints++;
                }
    
                //이 대여에 대한 요금 계산 결과 표시
                result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "\n";
                totalAmount += each.getCharge();
            }
    
            //풋터 추가
            result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
            result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
            return result;
        }

     

     

     

    이제 포인트 계산 부분 을 정리해보자 

    ① 이 부분도 대여기간 요소를 사용하고 있다. rental클래스로 옮겨서 작성해주자. 

    public class Rental {
    
        public int getFrequentRentalPoint() {
            if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) && (getDaysRented() > 1)) {
                return 2;
            }
    
            return 1;
        }
    }
        public String statement() {
            double totalAmount = 0;
            int frequentRenterPoints = 0;
            Enumeration rentals = _rentals.elements();
            String result = "Rental Record for " + getName() + "\n";
    
            while (rentals.hasMoreElements()) {
                Rental each = (Rental) rentals.nextElement();
    
                // 포인트를 제공하고 최신 영화를 이틀 이상 대여하는 경우 추가 포인트 제공
                frequentRenterPoints += each.getFrequentRentalPoint();

     

     

    이제 임시변수를 제거 해주자.

    위에서 thisAmount를 제거하듯 이번엔 totalAmount와 frequentRentalPoints라는 임시변수를 제거해주자. 임시변수는 길고 복잡한 루틴을 양산할 우려가 있으므로 제거해주도록 하자. 이것을 query Method를 이용하여 클래스내의 어떤 메소드에서도 접근가능하도록 깔끔한 디자인을 만들자. 

        public String statement() {
            Enumeration rentals = _rentals.elements();
            String result = "Rental Record for " + getName() + "\n";
    
            while (rentals.hasMoreElements()) {
                Rental each = (Rental) rentals.nextElement();
                
    
                //이 대여에 대한 요금 계산 결과 표시
                result +=
                    "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "\n";
            }
    
            //풋터 추가
            result += "Amount owed is " + String.valueOf(getTotalAmount()) + "\n";
            result += "You earned " + String.valueOf(getFrequentRentalPoints()) + " frequent renter points";
            return result;
        }
    
        private double getTotalAmount() {
            double totalAmount = 0;
            Enumeration rentals = _rentals.elements();
            while (rentals.hasMoreElements()) {
                Rental each = (Rental) rentals.nextElement();
                totalAmount += each.getCharge();
            }
            return totalAmount;
        }
    
        private int getFrequentRentalPoints() {
            int frequentRenterPoints = 0;
            Enumeration rentals = _rentals.elements();
            while (rentals.hasMoreElements()) {
                Rental each = (Rental) rentals.nextElement();
                frequentRenterPoints += each.getFrequentRentalPoint();
    
            }
            return frequentRenterPoints;
        }

     

    여기서 리팩토링을 통해 단 하나의 while문이 3번의 루프로 증가했는데 프로파일을 보기 전에는 계싼을 위해 루프가 얼마나 많은 시간을 소모할 지, 시스템의 전체 퍼포먼스에 영향을 줄 만큼 자주 호출될지는 모르니 while문 리팩토링을 걱정할 필요는 없다. 

     

     

     

    이제 조건문을 다형성으로 바꿔보자. 

    여기서 switch 구문은 자신의 데이터(rental)를 사용하는 것이 아닌 다른 객체의 속성(movie)에 기반하여 작성되고 있다. switch문을 사용한다면 자신의 데이터를 사용해야지 다른 객체의 데이터를 사용해서는 안된다. 이것은 즉 getCharge메소드를 movie 클래스로 옮겨야 함을 나타내고 있다. (앞으로 있을 변경사항이 새로운 영화를 추가하는 것이기 때문에 영화 종류를 rental로 넘겼을 때보다 영화종류변경의 파장을 줄이기 위해서 rental의 대여기간을 movie로 옮겨주는 것이 좋다)

    public class Movie {
    
        public double getCharge(int _daysRented) {
            double amount = 0;
    
            switch (getPriceCode()) {
                case Movie.REGULAR:
                    amount += 2;
                    if (_daysRented > 2) {
                        amount += (_daysRented - 2) * 1.5;
                    }
                    break;
                case Movie.NEW_RELEASE:
                    amount += _daysRented * 3;
                    break;
                case Movie.CHILDRENS:
                    amount += 1.5;
                    if (_daysRented > 3) {
                        amount += (_daysRented - 3) * 1.5;
                    }
                    break;
            }
            return amount;
        }
    }
    public class Rental {
    
        public double getCharge() {
            return _movie.getCharge(_daysRented);
        }
    }

     

    포인트 계산도 새로운 영화 종류에 따라서 변경되는 요소가 된다. 이것 또한 movie로 변경해주자. 

    public class Movie {
    
        public int getFrequentRentalPoint(int _daysRented) {
            if ((getPriceCode() == Movie.NEW_RELEASE) && (_daysRented > 1)) {
                return 2;
            }
    
            return 1;
        }
    }
    public class Rental {
    
        public int getFrequentRentalPoint() {
            return _movie.getFrequentRentalPoint(_daysRented);
        }
    }

     

     

     

    이제 영화 종류에 따른 변경사항을 적용하기 위해서 switch구문을 상속 을 이용하도록 바꾸자. 

    먼저 영화 종류마다 regularMovie, childrenMovie...의 종류로 만들어 movie에 상속시키는 방법이 있다. 하지만 이 방법에는 큰 결점이 있는데 바로 같은 영화일지라도 존속기간동안 클래스를 바꿀 수 없다는 것이다. 만약 newReleaseMovie에서 일정 기간이 지나면 regularMovie로 변경해야 하는데 이것이 불가능 하다는 것이다. 따라서 이 문제를 해결하기 위해서 스테이트 패턴 을 이용할 것이다. 스테이트 패턴은 간접적인 방법을 이용해서 요금코드객체에서 서브클래싱을 하고 필요하다면 언제든 요금을 바꾸는 방법으로 영화 종류가 바뀌도록 하는 것이다. (Movie안의 Price객체, price객체를 상속받는 regularPrice, childrenPrice...을 구현)

     

    ① 먼저 추상메소드를 가진 추상클래스인 Price객체를 만들고, 이를 상속받는 새로운 클래스들을 생성해준다. 

    abstract class Price {
        abstract int getPriceCode();
    }
    class ChildrenPrice extends Price{
    
        @Override
        int getPriceCode() {
            return Movie.CHILDRENS;
        }
    }
    class NewReleasePrice extends Price{
    
        @Override
        int getPriceCode() {
            return Movie.NEW_RELEASE;
        }
    }
    class RegularPrice extends Price{
    
        @Override
        int getPriceCode() {
            return Movie.REGULAR;
        }
    }

     

     

    ② Movie객체에서 int _priceCode를 Price객체로 변경해주고 이를 생성자 주입할 때 set메소드를 사용하여 price 객체를 주입하도록 set메소드를 변경해준다. 

    public class Movie {
    
        public static final int CHILDRENS = 2;
        public static final int REGULAR = 0;
        public static final int NEW_RELEASE = 1;
        private Price _price;
    
        public Movie(String title, int priceCode) {
            _title = title;
            setPriceCode(priceCode);
        }
    
        public int getPriceCode() {
            return _price.getPriceCode();
        }
    
        public void setPriceCode(int arg) {
            switch (arg){
                case REGULAR:
                    _price = new RegularPrice();
                    break;
                case CHILDRENS:
                    _price = new ChildrenPrice();
                    break;
                case NEW_RELEASE:
                    _price = new NewReleasePrice();
                    break;
                default:
                    throw new IllegalStateException("Incorrect Price Code");
            }
    
        }

     

    ③ 이제 movie의 getCharge 메소드를 price 객체안의 메소드로 옮겨주자. 

    abstract class Price {
        abstract int getPriceCode();
        public double getCharge(int _daysRented) {
            double amount = 0;
    
            switch (getPriceCode()) {
                case Movie.REGULAR:
                    amount += 2;
                    if (_daysRented > 2) {
                        amount += (_daysRented - 2) * 1.5;
                    }
                    break;
                case Movie.NEW_RELEASE:
                    amount += _daysRented * 3;
                    break;
                case Movie.CHILDRENS:
                    amount += 1.5;
                    if (_daysRented > 3) {
                        amount += (_daysRented - 3) * 1.5;
                    }
                    break;
            }
            return amount;
        }
    }

     

    ④ 추상클래스에 작성된 getCharge메소드를 구현 클래스에서 나타내도록 조건문 다형성 을 이용하도록 수정해주자. 

    abstract class Price {
        abstract int getPriceCode();
        abstract double getCharge(int _daysRented);
    }
    class ChildrenPrice extends Price{
    
        @Override
        double getCharge(int _daysRented) {
            double amount = 1.5;
            if (_daysRented > 3) {
                amount += (_daysRented - 3) * 1.5;
            }
            return amount;
        }
    |
    class NewReleasePrice extends Price{
    
        @Override
        double getCharge(int _daysRented) {
            return _daysRented * 3;
        }
    }
    class RegularPrice extends Price{
    
        @Override
        double getCharge(int _daysRented) {
            double amount = 2;
            if (_daysRented > 2) {
                amount += (_daysRented - 2) * 1.5;
            }
            return amount;
        }
    }
    public class Movie {
    
        public double getCharge(int _daysRented) {
           return _price.getCharge(_daysRented);
        }
    }

     

    ⑤ getFrequentRentalPoints메서드에도 똑같이 적용해준다. (new release면 추가 포인트제공) 그러나 이 경우에는 중복되는 return 1이 존재하므로 abstract로 구현하지 않고 public메소드로 적용해준다. (달라지는 new release에만 다시 구현)

    abstract class Price {
    
        public int getFrequentRentalPoint(int _daysRented){
            return 1;
        }
    }
    class NewReleasePrice extends Price {
    
        public int getFrequentRentalPoint(int _daysRented) {
            return (_daysRented > 1) ? 2 : 1;
        }
    }
    public class Movie {
    
        public int getFrequentRentalPoint(int _daysRented) {
           return _price.getFrequentRentalPoint(_daysRented);
        }
    }

     

    스테이트 패턴을 적용하여 조건문을 상속을 통한 다형성 구현을 통해서 영화의 종류가 바뀌더라도 쉽게 변경사항을 반영할 수 있는 코드로 바꾸었다. 

    'JAVA > Java' 카테고리의 다른 글

    자바에서 static의 사용  (0) 2021.12.30
    Method Reference (Java 8)  (0) 2021.12.19
    DTO와 VO 그리고 Entity  (0) 2021.12.13
    Setter의 사용 금지  (0) 2021.12.13
    계층별, 기능별 패키지 구성  (0) 2021.12.12
Designed by Tistory.