Java Refactoring -4, 매개변수 남용으로 거대해진 메서드 개선

박태건·2021년 7월 17일
0

리팩토링-자바

목록 보기
4/13
post-thumbnail

레거시 코드를 클린 코드로 누구나 쉽게, 리팩토링

위 책을 보면서 정리한 글입니다.

매개변수 남용으로 거대해진 메서드 개선하기

많은 기능을 한 번에 처리하려고 할수록 문제가 발생햇을 때 해결하기 어렵다.

  • 메서드는 전달받은 매개변수를 이용하여 비즈니스 로직을 수행하는 역할
  • 매개변수를 남용하면 다양한 문제가 발생
    • 해당 메서드의 사용이 어려워지고
    • 코드의 가독성이 나빠지며
    • 메서드 오버로딩 등

수 많은 매개변수 때문에 문제가 발생한 코드를 찾기가 어렵다.

개선방향

많은 기능이 혼재된 메서드는 '메서드 추출'로 기능을 분리하고 매개변수의 수를 줄여 명확하게 한다.

  • 이렇게 개선된 코드는 유연성과 가독성을 확보
  • 메서드를 호출하는 부분에서도 요청 방식이 '규격화'되어 메서드를 사용할 때나 수정할 때에 유연성을 높힐 수 있다.

메서드 추출

매개변수가 많지만, 메서드 추출을 위한 기능이 명확하지 않을 때

  • 매개변수가 많다는 것 : 메서드에서 필요한 값이 많다 --> 많은 기능을 처리하고 있을 '가능성'이 높다.
    • 매개변수가 많다고 반드시 고쳐야 하는 것은 아니다
  • 직관적으로 보이는 매개변수에서 개선에 대한 가능성을 생각해보고, 시도해서 개선 사항이 나타나지 않으면 과감하게 넘기는 것이 필요

메서드 기능이 혼재되어 있고, 매개변수가 많을 때 어떤 문제가 생겨서 수정이 필요할까

  • 객체 지향의 다섯 원칙 중 하나인 '단일 책임의 원칙(SRP, The Single Responsibility Pirnciple)
    • 메서드 기능이 혼재되어 있으면 한 기능이 수정이 필요할 때마다 메서드 내의 모든 기능에 대한 검증 테스트가 요구
    • 메서드의 수정의 유연성이 떨어지며 많은 유지보수 비용이 발생
  • 기능이 많아지면, 요구되는 매개변수가 늘어난다.
    • 늘어난 매개변수는 메서드의 사용을 어렵게 하고, 제한함으로써 메서드의 재사용성을 떨어뜨린다.
  • 메서드 호출시에도 인자가 모두 사용되지 않는 경우가 생긴다.
    • 불필요한 인자의 전달과, 이에 대한 예외 코드가 필요해지며 코드를 복잡하게 만든다.
    • 복잡해진 코드는 개발자의 실수로 인한 내재적인 오류를 포함할 가능성을 높인다.
  • 복잡해진 코드의 기능을 명확히 보여주지 못하는 무분별한 매개변수
    • 가독성이 떨어지며, 복잡하고 명확하지 못한 테스트 케이스를 발생시킨다.

레거시 코드

TotalPaymentGate 클래스

public class TotalPaymentGate {
    
    // 1. 객실 번호, 투숙 기간을 통한 예약 결제 기능 체크

    // 2. 결제 방법에 따른 결제 진행

    // 3. 결제 완료 후 예약 결제 정보 저장

    // 전달 매개변수
    /**
     * 
     * @param payMethod         // 결제 방법
     * @param price             // 결제 금액              
     * @param roomNum           // 예약 객실 번호
     * @param duringDay         // 투숙 기간
     * @param reservationName   // 예약자명
     * @param password          // 예약 비밀번호
     * @return                  // PayResultTO
     */
    public PayResultTO requsetPay(String payMethod, int price, String roomNum, int duringDay, String reservationName, String password) {
        DBManager dbManager = new DBManager();

        // 1. roomNum, duringDay로 예약 결제 가능 체크
        boolean isReservation = dbManger.checkRoomReservation(roomNum, duringDay);
        
        PayResultTO payResult = null;

        if(!isReservation) {
            // 2. 결제 수단 객체 생성 및 결제 진행
            Requestor  requestor = RequstorFactory.createRequestor(payMethod);
            Map<String, Object> result = requestor.requset(price, roomNum);
            payResult = parsePayResultTO(payMethod, result);

            // 3. 정상 결제 완료 후 reservationName, roomName, password로 예약 정보 저장
            dbManager.saveRoomReservation(payMethod, price, roomNum, duringDay, reservationName, password);
            
        }

        return payResult;
    }
}

위 코드의 문제점

  • requestPay() 메서드에 여러 기능이 혼재
    • 이런 상태에서 예약 결제에 결제 방법 추가와 같은 기능 수정이 발생하면 메서드는 점점 수정이 어려운 상태로 변하게 된다.

레거시 코드 개선 과정

3가지 기능이 혼재된 클래스에서, 메서드 추출을 통해 각 기능을 분리, 각 기능에서 요구되는 매개변수를 분리.

개선

ReservationChecker (예약 가능 체크 요청)
-- 전달 매개변수 : 예약 정보 객체, 객실 번호, 투숙 기간
-- 분리된 기능 : 객실 번호, 투숙 기간으로 예약 결제 가능 체크

TotalPaymentGate (결제 요청)
-- 전달 매개변수 : 예약 정보 객체, 금액, 결제 정보
-- 분리된 기능 : 결제 방법에 따른 결제 진행

ReservationStorage (예약 정보 저장 요청)
-- 전달 매개변수 : 예약 정보 객체, 금액, 결제 방법, 객실 번호, 투숙 기간, 예약 비밀번호, 예약자명
-- 분리된 기능 : 결제 완료 후 예약 결제 정보 저장

이전에는 한 기능이 변경되거나, 요구되는 매개변수가 변경되면 메서드 내의 모든 기능에 대한 검증이 필요했다.

  • 전달되는 매개변수가 많아짐에 따라 6개의 매개변수에 대한 예외처리가 구현되어야 하고
  • 이때문에 복잡해진 코드는 가독성이 현저하게 떨어져, 수정 또한 어려워지게 되어 개발자의 실수로 인한 오류가 발생할 가능성이 높아진다.
  • 각 매개변수가 전달될 경우의 수만큼 복잡한 테스트 케이스가 요구되어 한 메서드에 대한 테스트 코드가 거대해 지고 불명확해진다.

개선 후, TotalPaymentGate 클래스는 결제 기능만 수행하고, 매개변수 또한 결제에 필요한 정보만 전달한다.

  • TotalPaymentGate 클래스에 있던 예약 결제 가능 체크와, 예약 정보 저장 기능은 메서드 추출을 통해 각각의 클래스로 분리
  • 기능의 분리와 함께 불명확한 메서드 인자 남용을 개선하여 레거시 코드에서 드러난 문제를 해결

개선 과정

단위 테스트 미포함

  1. 예약 결제 가능 체크 기능에 대한 메서드 추출
  2. 예약 정보 기능에 대한 메서드 추출

단위 테스트 포함

  1. 추출할 메서드에 대한 단위 테스트 작성
  2. 추출 메서드에 해당하는 객체와 메서드 생성
  3. 기존 비즈니스 로직을 추출하여 이동
  4. 단위 테스트를 작동하여 통과 확인

ReservationChekcerTest 클래스

public class ReservationChekcerTest extends TestCase {
    @Test
    public void testCheckReservatiopRoom() {
        //Given
        ReservationChecker reservationChecker = new ReservationChecker();
        String roomNum = "2408";
        int duringDay = 3;
        
        //When
        boolean isReservation = reservationChecker.checkReservationRoom(roomNum, duringDay);

        //Then
        assertEquals(isReservation, false);
    }
}

ReservationChecker 클래스

public class ReservationChecker {
    public boolean checkReservationRoom(String roomNum, int duringDay) {
        DBManager dbManger = new DBManager();

        // roomNum, duringDay로 예약 가능 체크
        return dbManager.checkRoomReservation(roomNum, duringDay);
    }
}

ReservationCheckerTest 클래스

public class ReservationCheckerTest extends TestCase {
   @Test
   public void testSaveRoomReservation() {
       //Given
       ReservationStorage reservationStorage = new ReservationStorage();
       String roomNum = "2408";
       int duringDay = 3;
       String payMethod = "Card";
       int price = 200000;
       String reservationName = 'James';
       String password = "1234";

       //When
       boolean isSuccess = reservationStorage.saveRoomReservation(payMethod, price, roomNum, reservationName, passWord);

       //Then
       assertEqulas(isSuccess, true);
   }
}

ReservationStorage 클래스

public class ReservationStorage {
    public boolean saveRoomReservation(String payMethod, int price, String roomNum, String reservationName, String passWord) {
        DBManager dbManager = new DBManager();
        
        // 정상 결제 완료 후 reservationName, roomName, password로 예약 정보 저장
        boolean isSuccess = dbManager.saveRoomReservation(payMethod, price, roomNum, duringDay, reservationName, password);
        return isSuccess;
    }
}

TotalPaymentGate 클래스

public class TotalPaymentGate {
    //  결제 방법에 따른 결제 진행

    // 전달 매개변수
    /**
     * 
     * @param payMethod         // 결제 방법
     * @param price             // 결제 금액              
     * @return                  // PayResultTO
     */
    public PayResultTO requsetPay(String payMethod, int price) {
        DBManager dbManager = new DBManager();
        PayResultTO payResult = null;

        // 결제 수단 객체 생성 및 결제 진행
        Requestor  requestor = RequstorFactory.createRequestor(payMethod);
        Map<String, Object> result = requestor.requset(price, roomNum);
        payResult = parsePayResultTO(payMethod, result);

        return payResult;
    }
}

개선된 레거시 코드

ReservationChecker 클래스

  • TotalPaymentGate의 기능을 추출하여 만든 클래스
  • 객실번호, 투숙 기간을 이용하여 예약 결제가 가능한지 체크하는 기능을 담당

TotalPaymentGate 클래스

  • 기존의 여러 기능이 추출되고, 예약 결제 기능만 존재하는 클래스

ReservationStorage 클래스

  • TotalPaymentGate의 기능을 추출하여 만든 클래스
  • 정상적인 결제 완료 후, 예약자명, 객실 번호, 예약 비밀번호를 이용하여 예약 정보를 저장하는 기능을 담당.

요약 및 정리

메서드 추출을 통한 개선의 이점

  • 메서드 추출을 함으로써 메서드에 혼재된 기능을 분리 가능
    • 객체의 5대 원칙 중 SRP를 지켜서 해당 객체가 한 가지 책임에 집중하기 때문에, 수정에 관한 유연서을 가지게 된다.
  • 혼재된 기능이 분리되면서 여러 기능을 모두 처리하기 위해 남용되던 메서드 인자 또한 분리되고 명확해진다.
    • 메서드를 사용하는 클라이언트는 명확하게 원하는 기능의 메서드를 호출하여 사용가능하고, 기능을 수정할 메서드에만 집중가능한 장점이 있다.

매개변수 남용으로 거대해진 메서드를 개선하기 위한 생각의 흐름

  1. 메서드에 여러 기능이 혼재되어 있지 않은지 확인
  2. 전달되지 않은 메서드 인자가 혼재된 기능에 사용되는지 확인
  3. 혼재된 기능은 메서드 추출을 통하여 분리
profile
노드 리액트 스프링 자바 등 웹개발에 관심이 많은 초보 개발자 입니다

0개의 댓글