Java Refactoring -5, 비즈니스 로직과 기능 호출이 섞여 있는 메서드 개선하기

박태건·2021년 7월 17일
0

리팩토링-자바

목록 보기
5/13
post-thumbnail

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

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

비즈니스 로직과 기능 호출이 섞여 있는 메서드 개선

코드의 가독성을 위해 분석해야할 영역을 일관성 있게 만드는 것이 매우 중요하다.

  • 객체의 행위 : 메서드
  • 기능 처리를 메서드에 직접 구현하거나, 이미 구현된 객체의 메서드를 호출하여 구현하기도 한다.
  • 가독성을 전혀 고려하지 않은, 비즈니스 로지과 기능 호출이 뒤섞인 메서드는 잠재적인 문제가 존재.
    • 메서드에 구현된 코드가 가독성이 나빠 분석하는데 비용이 발생
    • 한 가지의 기능을 처리하기 위한, 비즈니스 로직과 기능 호출이 혼합된 메서드는 정상적으로 작동해서 문제로 인식되지 않지만 유지보수에 어려움을 느낄 수 있다 (가독성)
    • 코드를 분석하는 사람은 기능에 해당하는 메서드의 코드만을 이해하여 기능을 분석하길 원한다.

개선방향

로직과 기능 호출을 일관되게 수정하여 분석하기 쉽게 만들자

  • 효율적인 유지보수를 위해 개선된 모습은 Client 로직에서 호출하는 메서드의 코드만 확인하여 원하는 기능을 분석할 수 있는 것.
  • 하나의 메서드에 구성된 복잡하고 많은 비즈니스 로직을 각각 하나의 메서드로 추출 후, 기능 호출로만 구성된 메서드로 개선.

코드의 가독성

Client에서 호출하는 기능을 분석하기 위해 구성된 모든 비즈니스 로직과 호출되는 기능의 내부까지 다 분석해야 하는가, 섞여 있어도 상관 없지 않나?

  • 기능 분석시 모든 부분을 다 분석하는 것은 시간 대비 비효율적.
    • 코드의 가독성은 의미뿐만 아니라 비용에서도 아주 중요
    • 한 메서드를 분석하기 위해 한 코드 영역을 분석하는 것과 여러 객체의 메서드, 내부 메서드, 비즈니스 로직 영역을 넘나들며 분석하는 것은 큰 차이가 있다.

레거시 코드

Send 클래스의 sendResaurantEventSMS() 메서드의 기능은 다음과 같은 복잡한 비즈니스 로직으로 구성되어 있다.

sendRestaurantEventSMS() 메서드의 기능 정의
1. 호텔 고객 정보 추출(기능 호출)
2. 추출된 고객 정보 중 레스토랑 이용 고객 정보 추출(비즈니스 로직)
3. 레스토랑 이용 고객 중 문자 수신 동의 고객 추출(기능 호출)
4. 문자 수신 동의 고객별 맞춤 메시지 생성(비즈니스 로직)
5. 문자 발송(기능 호출)

Send 클래스

public class Send {
    public void sendRestaurantEventSMS() [
        List<Member> members = null;

        // 기능 1. 호텔 고객 정보 추출
        DBManager dbManager = new DBManager();
        members = dbManager.getMember();

        // 기능 2. 추출된 고객 정보 중 레스토랑 이용 고객 정보 호출
        List<Member> restaurantMembers = new ArrayList<Member>();

        for(Member member : members) {
            if(member.isRestaurantMember()) {
                restaurantMembers.add(member);
            }
        }

        // 기능 3. 레스토랑 이용 고객 중 문자 수신 동의 고객 추출
        List<Member> smsMembers = getSmsMember(restaurantMembers);

        // 기능 4. 문자 수신 동의 고객별 맞춤 메시지 생성
        for(int i=0; i<smsMembers.size(); i++) {
            if("royal".equals(smsMembers.get(i).getGrade())) {
                // 메시지 생성 및 세팅 로직
                // 중량
            } else if("basic".equals(smsMembers.get(i).getGrade())) {
                // 메시지 생성 및 세팅 로직
                // 중략
            } else {
                // 잘못된 등급 표시 로직
                // 중략
            }
        }

        // 기능 5. 정렬된 문자 발송 로직
        SMSManager smsManager = new SMSManager();
        smsManager.sendSMS(smsMembers);
    ]

    public List<Member> getSmsMember(List<Member> restaurantMembers) {
        List<Member> smsMember = new ArrayList<Member>();

        for(Member member : restaurantMembers) {
            smsMember = divideSmsMember(smsMember, member);
        }

        return smsMember;
    }

    priivate List<Member> divideSmsMember(List<Member> smsMembers, Member member) {
        if(checkSMSAgree(member)) {
            smsMembers.add(member);
        }

        return smsMembers;
    }
}

위 코드의 문제점

  • sendRestaurantEventSMS() 메서드의 기능을 분석하기 위해서는 메서드 영역만 분석하는 것이 아닌, 내부에 구현된 다른 객체의 메서드 영역, 내부 메서드 영역, 비즈니스 로직을 모두 파악하고 분석해야 한다.
  • 코드를 분석하기 어렵고, 가독성이 현저히 떨어지게 된다.
  • 이런식의 코드는 기능 확장, 수정 등의 유지보수 때마다 분석에 많은 비용이 들게 된다.

레거시 코드 개선 과정

메서드 추출로 복잡한 코드 분석이 필요한 메서드를 가독성이 높은 메서드로 개선

  • 비즈니스 로직과 메서드 호출을 MemberManager 객체에 위임하여 sendRestaurantEventSMS()는 MemberManager 영역만 확인하면 알 수 있게 개선
  • 기본적으로 메서드 추출을 이용하여 기능을 한 객체에 위임하고 분석 영역을 일관서 있게 만드는 것
  • sendRestaurntEvenSMS()에서는 5가지의 기능에 대한 메서드 호출만 확인하면 된다.
    • 호출되는 메서드의 코드 분석이 필요하면 MemberManager 영역에서 분석 진행
    • 5가지 호출 기능의 영역은 MemberManager 영역으로 동일하므로 코드 분석이 용이.

1차 기능 개선

  1. 단위 테스트로 MemberManager 클래스를 생성한 후 getMember() 메서드를 생성.
  2. sendRestaurntSMS() 메서드에서 DBManger 클래스를 활용하여 정보를 반환받는 부분을 getMember() 메서드로 추출
  3. 단위 테스트의 정상 작동을 확인.

MemeberManagerTest 클래스

public class MemeberManagerTest extends TestCase {
    @Tset
    public void testGetMember() {
        // Given
        MemberManger memberManager = new MemberMangaer();

        // When
        List<Member> result = memberManager.getMember();

        // Then
        assertNotNull(result);
    }
}

MemberManager 클래스

public class MemberManager {
    public List<Member> getMember() {
        DBManger dbManager = new DBManger();
        List<Member> members = dbManger.getMember();
        return members;
    }
}

2차 기능 개선

  1. 단위 테스트로 MemberManger 클래스의 getRestaurantMembers() 메서드를 생성
  2. 선별된 호텔 가입자 중에서 레스토랑 이용 고객을 다시 선별하는 sendRestaurantSMS() 메서드의 해당 로직을 getRestaurantMembers() 메서드로 추출
  3. 단위 테스트의 정상 작동을 확인.

MemberManagerTest 클래스

public class MemberManagerTest extends TestCase {
   @Test
   public void testGetMember() {
       // 중략.
   }

   @Test
   public void testGetRestaruntMembers() {
       // Given
       MemberManager memberManager = new MemberManager();
       List<Member> members = memberManager.getMember();

       // When
       List<Member> result = memberManager.testGetRestaruntMembers(members);

       // Then
       assertNotNull(result);
   }
}

MemberManager 클래스

public class MemberManager {
	public List<Member> getMember() {
        // 중략.
    }
    
    public List<Member> getRestarauntMembers(List<Member> members) {
        List<Member> restaurntMembers = new ArrayList<Member>();

        for(Member member : members) {
            if(member.isRestaurnt()) {
                restaurntMembers.add(member);
            }
        }

        return restaurntMembers;
    }
}

3차 기능 개선

  1. 단위 테스트로 MemberManger 클래스의 getSmsMembers() 메서드를 생성
  2. 선별된 고객 문자 수신에 동의한 고객을 다시 선별하는 getSmsMebers()는 Send 클래스의 내부 메서드였으므로, 이 부분을 MemberManager로 이동한 후 단위 테스트의 정상 작동을 확인

MemberManagerTest 클래스

public class MemberManagerTest extends TestCase {
    @Test
    public void testGetMember() {
        // 중략
    }

    @Test
    public void testGetRestaurntMembers() {
        // 중략
    }

    @Test
    public void testGetSmsMember() {
        // Given
        MemberManger memberManager = new MemberManager();
        List<Member> members = memberManager.getMember();
        List<Member> restaruntMembers = memberManager.getRestaurantMembers(membres);

        // When
        List<Member> result = memberManager.getSmsMember(restaruntMembers);

        // Then
        assertNotNull(result);
    }
}

MemberManager

public class MemberManager {
	public List<Member> getMember() {
        // 중략.
    }
    
    public List<Member> getRestarauntMembers(List<Member> members) {
        // 중략.
    }
    
    public List<Member> getSmsMember(List<Member> restaurntMembers) {
        List<Member> smsMember = new ArrayList<Member>();

        for(Member member : restaurntMembers) {
            smsMember = divideSmsMember(smsMember, member);
        }
        return smsMember;
    }

    private List<Member> divideSmsMember(List<Member> smsMembers, Member member) {
        if(member.isSms()) {
            smsMembers.add(member);
        }
        return smsMembers;
    }
}

4차 기능 개선

  1. 단위 테스트로 MemberManager 클래스의 getCustomMessage() 메서드를 생성
  2. 고객에게 맞는 메시지를 세팅하는 sendRestaurntSMS() 메서드의 해당 로직을 getCustomMessage() 로 추출.
  3. 단위 테스트의 정상 작동을 확인.

MemberManagerTest 클래스

public class MemberManagerTest extends TestCase {
    @Test
    public void testGetMember() {
        // 중략
    }

    @Test
    public void testGetRestaurntMembers() {
        // 중략
    }

    @Test
    public void testGetSmsMember() {
        // 중략
    }

    @Test
    public void testSetCustomMessage() {
        // Given
        MemberManger memberManager = new MemberManager();
        List<Member> members = memberManager.getMember();
        List<Member> restaruntMembers = memberManager.getRestaurantMembers(membres);
        List<Member> smsMembers = memeberManger.getSmsMembers(restaruntMembers);

        // When
        List<Member> result = memberManager.setCustomMessage(smsMembers);

        // Then
        assertNotNull(result);
    }
}

MemberManager 클래스

public class MemberManager {
	public List<Member> getMember() {
        // 중략
    }
    
    public List<Member> getRestarauntMembers(List<Member> members) {
        // 중략
    }
    
    public List<Member> getSmsMember(List<Member> restaurntMembers) {
        // 중략
    }

    private List<Member> divideSmsMember(List<Member> smsMembers, Member member) {
        // 중략
    }

    public List<Member> setCustomMessage(List<Member> smsMembers) {
        // 기능 4. 문자 수신 동의 고객별 맞춤 메시지 생성
        for(int i=0; i<smsMembers.size(); i++) {
            if("royal".equals(smsMembers.get(i).getGrade())) {
                // 메시지 생성 및 세팅 로직
                // 중량
            } else if("basic".equals(smsMembers.get(i).getGrade())) {
                // 메시지 생성 및 세팅 로직
                // 중략
            } else {
                // 잘못된 등급 표시 로직
                // 중략
            }
        }

        return smsMembers;
    }
}

5차 기능 개선

  1. 단위 테스트로 MemberManager 클래스의 sendSMS() 메서드를 생성
  2. SMSManager 클래스에 구현된 문자 발송 기능이지만, 분석 영역을 동일하게 하기 위해 MemberManger로 메서드 추출
  3. 단위 테스트의 정상 작동을 확인.

MemberManagerTest 클래스

public class MemberManagerTest extends TestCase {
    @Test
    public void testGetMember() {
        // 중략
    }

    @Test
    public void testGetRestaurntMembers() {
        // 중략
    }

    @Test
    public void testGetSmsMember() {
        // 중략
    }

	@Test
    public void testSetCustomMessage() {
        // 중략
    }
    
    @Test
    public void testSetCustomMessage() {
        // Given
        MemberManger memberManager = new MemberManager();
        List<Member> members = memberManager.getMember();
        List<Member> restaruntMembers = memberManager.getRestaurantMembers(membres);
        List<Member> smsMembers = memeberManger.getSmsMembers(restaruntMembers);
		List<Member> customMembers = memberManager.setCustomMessage(smsMembers);
        
        // When
        boolean result = memberManager.sendSMS(customMembers);

        // Then
        assertEqauls(true, result);
    }
}

MemberManager 클래스

public class MemberManager {
	public List<Member> getMember() {
        // 중략
    }
    
    public List<Member> getRestarauntMembers(List<Member> members) {
        // 중략
    }
    
    public List<Member> getSmsMember(List<Member> restaurntMembers) {
        // 중략
    }

    private List<Member> divideSmsMember(List<Member> smsMembers, Member member) {
        // 중략
    }

    public List<Member> setCustomMessage(List<Member> smsMembers) {
        // 중략
    }
    
    public boolean sendSMS(List<Member> smsMembers) {
    	SMSManager smsManager = new SMSManager();
        boolean result = smsManager.sendSMS(smsMembers);
        return result;
    }
}

Send 클래스

public class Send {
    public void sendRestaurantEventSMS() [
        MemberManger memberManager = new MemeberManager();

        // 기능 1. 호텔 고객 정보 추출
        List<Members> members = memberManager.getMember();
        
        // 기능 2. 추출된 고객 정보 중 레스토랑 이용 고객 정보 호출
        List<Member> restaurantMembers = memberManager.getRestaurntMembers(members);

        // 기능 3. 문자 수신 동의 고객 추출
        List<Member> smsMembers = memberManger.getSmsMember(restaruntMembers);

        // 기능 4. 문자 수신 동의 고객별 맞춤 메시지 생성
		List<Member> customSMSMembers = memberManger.setCustomMessage(smsMembers);

        // 기능 5. 정렬된 문자 발송 로직
        boolean result = memberManager.sendSMS(customSMSMembers);
    }
}

개선된 레거시 코드

Send 클래스

  • 문자 발송을 담당하는 클래스로 문자 발송에 필요한 기능은 MemberManger의 기능 호출로만 구성되어 코드 가독성 및 분석 영역을 일관성 있게 개선

MemberManger 클래스

  • 고객 정보 처리를 담당하는 클래스로, 고객 추출, 레스토랑 이용 고객 추출, 고객 맞춤 메시지 구성, 문자 발송 등 고객에 대한 기능을 담당하는 객체
  • Send 클래스의 문자 발송 기능은 해당 객체를 통해 수행하도록 개선

요약 및 정리

메서드 이동과 추출을 이용해서 코드 분석의 영역을 일관성 있게 모아 해결

  • 구조 면에서는 크게 달라지지 않았지만, 해당 레거시 코드의 유지보수를 맡게 된다면 각가에 해당하는 메서드에 대한 분석이 용이해짐.
  • 기존 메서드도 간결해졋고, 코드 분석 영역이 일관성 있게 변경

메서드 추출로 분석 영역의 일관성 유지하기에 대한 생각의 흐름

  1. 한 메서드에서 로직의 흐름이 일관성이 있는지 체크(비즈니스 로직과 기능 호출 혼재)
  2. 해당 메서드가 기능 호출이 필요한 메서드인 경우 호출되는 분석 영역을 체크(시퀀스 다이어그램처럼 각자 맞는 체크 방법으로 확인)
  3. 혼재된 비즈니스 로직을 해당 메서드가 담당할 객체로 메서드 추출
  4. 기존 메서드를 담당 객체의 기능 호출로 구성
profile
노드 리액트 스프링 자바 등 웹개발에 관심이 많은 초보 개발자 입니다

0개의 댓글