Java Refactoring -11, 연동 규약에 종속된 구조 개선

박태건·2021년 7월 22일
1

리팩토링-자바

목록 보기
11/13
post-thumbnail
post-custom-banner

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

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

연동 규약에 종속된 구조 개선

연동 규약에 너무 의존적인 기능은 연동 규약만은 바라볼 수 밖에 없다.

  • 다른 시스템과 연동하는 클라이언트 애플리케이션을 만들 때, 자연스럽게 연동 규약을 먼저 확인하게 된다.
  • 하지만 연동 규약을 중심으로 작성된 코드를 직접 호출하는 부문뿐만 아니라, 파서와 도메인 로직, UI까지 연동 규약이 깊에 관여한다.
    • 연동 규약의 type이나 status같은 선택적 매개변수가 로직 곳곳에 자리 잡는데
    • 그 결과 구현 애플리케이션 내에서 독립적인 로직이 아니나, 다른 시스템에 의해 type이나 status를 선택하기 위한 분기문의 수정이나 기능 추가가 어쩔 수 없이 생기게 된다.

개선방향

연동 규약에 포함된 타입에 맞는, 각 기능에 해당하는 클래스를 생성

연동 규약에 포함된 타입 및 상태에 따라 강하게 종속된 결합 구조를 다음과 같이 개선

  1. 각 타입 및 상태에 맞는 클래스를 생성하여 강한 결합 구조를 제거
  2. 중복된 코드와 기능을 추상화로 구현하여 클래스 간 계층화를 진행
  3. 객체 간 연동은 인터페이스 기반으로 모듈화를 진행

질문답

연동 규약을 고려해서 설계하는 것이 무조건 나쁜 것일까

  • 절대적으로 나쁜 방법은 없다. 다만, 상황에 따라 좋은 개선안이 있을 뿐, 연동 규약까지 분기문 없이 모든 구조가 전달된다면 나쁜 설계가 아니다.
    • 이번 장의 문제는, 연동 규약에 따른 강한 결합 구조를 가진 클래스가 발생하고 이를 코드에서 남용한다는 점이다.

각 상황에 대한 객체를 만들거나 메서드를 만드는 것은, 객체의 종류가 추가되면 많은 부분을 수정해야 하지 않을까

  • 맞는 말이다. 하지만 강한 결합구조를 클래스나 거대한 데이터 구조에서 수정하려면 예상되는 상황에 대한 도메인 로직을 파악하여 담당하는 클래스를 수정하는 작업을 거쳐야 한다.
    • 사용하는 부분과 사용하지 않는 기능에 대한 구분점이 없으므로, 이 역시 파악하고 중복해서 코드를 작성해야 한다.
    • 기존에 동작하던 클래스의 많은 부분을 수정하면 사이드 이펙트가 발생할 확률이 높아질 수 밖에 없다.
    • 하지만, 로직을 담당하는 클래스를 추가하는 것은 기존 로직과 상관없이 구현되므로 사이드 이펙트의 가능성이 줄어든다.
  • 이런 문제를 개선하기 위해서 인터페이스를 통해 객체의 계층 구조로 리팩토링 해야 한다.

인터페이스를 통한 계층 구조는 뭘 의미하는 걸까

  • 객체지향의 원칙 중 하나인 DIP(Dipendency Inversion Principle)을 기반으로 리팩토링 하는 방법이다.
    • DIP는 '추상화를 중심으로' 구현하는 원리라고 생각하면 된다.
    • 추상화를 구체화한 객체들을 추상화 객체 중심으로 구현하면 당연히 구체화 객체들을 탄력적으로 사용하게 되고 확장도 쉬어지기 때문.

레거시 코드

Inquire 클래스

  • 고객 화면을 관리하는 클래스
    • 조회를 요청하면 InquireRequestModel 객체의 request() 메서드를 호출
    • 요청 후, 응답한 데이터를 받아서 화면 출력을 담당하는 InquireRoomDisplayView 클래스에 전달
  • 고객이 객실 종류(비즈니스, 패밀리, 스위트)에 해당하는 버튼을 누르면 그애 맞는 조회를 실행
public class Inquire {

    public void businessInquireButtonClicked(String checkDate, String checkOutDate) {
        // 비즈니스 룸 요청
        InquireRoomItemVO inquireData = InquireRequsetModel.request(checkInDate, checkOutDate, RoomConstValue.ROOM_TYPE_BUSINESS);

        // 비즈니스 룸 요청 결과 출력
        InquireView.getInstance().display(RoomConstValue.ROOM_TYPE_BUSINESS, inquireDate);
    }

    public void familyInquireButtonClicked(String checkDate, String checkOutDate) {
        // 패밀리 룸 요청
        InquireRoomItemVO inquireData = InquireRequsetModel.request(checkInDate, checkOutDate, RoomConstValue.ROOM_TYPE_FAMILY);

        // 패밀리 룸 요청 결과 출력
        InquireView.getInstance().display(RoomConstValue.ROOM_TYPE_FAMILY, inquireDate);
    }

    public void suiteInquireButtonClicked(String checkDate, String checkOutDate) {
        // 스위트 룸 요청
        InquireRoomItemVO inquireData = InquireRequsetModel.request(checkInDate, checkOutDate, RoomConstValue.ROOM_TYPE_SUITE);

        // 스위트 룸 요청 결과 출력
        InquireView.getInstance().display(RoomConstValue.ROOM_TYPE_SUITE, inquireDate);
    }

}

InquireRequestModel 클래스

  • Inquire 클래스에서 조회 요청을 받으면 서버에 요청을 실행하고 응답을 받는다.
  • 응답받은 결과를 InquireRoomParser에 파싱 요청하고, 파싱 결과값을 Inquire 클래스에 다시 전달

  • InquireReuqetModel 클래스를 통해 서버에 요청하고 처리된 결과값을 InquireRoomVO를 받게 된다.
  • 이렇게 받은 데이터를 InquireView 객체에 전달하여 조회된 결과값을 고객에게 보여준다.
    • (View에 맞는 데이터를 반환하는 역할)
public class InquireRequestModel {
    public static InquireRoomItemVO request(String checkInDate, String checkOutDate, int type) {
        // 서버에 요청
        String response = HttpUtils.postInquireRequesta(makeRequestData(CheckInDate, checkOutDate, type));

        // 요청값을 파싱
        InquireRoomParser parser = new InquireRoomParser();
        parser.setResponseResult(response);

        return parser.parsing();
    }
}

InquireRoomVo 클래스

  • 화면 출력을 담당하는 데이터 객체
  • InquireRoomParser 클래스를 통해 만들어진다.
  • InquireRoomDisplayView 클래스에서 이 객체를 사용하여 고객 화면에 출력

  • 비즈니스 룸, 패밀리 룸, 스위트 룸에 대한 필요한 데이터를 필드로 가지고 있는 데이터 클래스, 객실마다 필요한 데이터가 약간씩 다르다.

  • 객실 종류에 맞게 InquireRoomParser 클래스가 데이터를 입력하고, 이 데이터를 기반오르 고객 화면에 출력.

  • 비즈니스룸패밀리룸스위트룸
    roomNumbe/floor/price/bedTyperoomNumber/floor/price/roomCount/roomStype/viewTyperoomNumber/floor/price/roomCount/roomStyle/viewType/optionalService
public class InquireRoomVO {
    private String roomNumber;
    private String floor;
    private String price;
    private String bedType;
    private int roomCount;
    private String roomStyle;
    private String viewType;
    private List<String> optionalService;
    // ... 중략
}

InquireRoomParser 클래스

  • InquireRequsetModel 클래스에서 전달된 응답값을 화면에 출력할 뷰에 맞게 파싱
  • 응답값은 Json 포맷, 이를 파싱하여 InquireRoomVO 객체를 만들어 반환
public class InquireRoomParser {
    // 중략

    public void setResponseResult(String response) {
        this.response = response;
    }

    public InquireRoomVO parsing() {
        JSONObject itemJsonObject = new JSONObject(this.response);
        String roomNumber = (itemJsonObject.has("room_number")) ? itemJsonObject.getString("room_number") : null;
        String floor = (itemJsonObject.has("floor")) ? itemJsonObject.getString("floor") : null;
        String price = (itemJsonObject.has("price")) ? itemJsonObject.getString("price") : null;
        String bedType = (itemJsonObject.has("bedType")) ? itemJsonObject.getString("bedType") : null;
        // 중략

        InquireRoomVO item = new InquireRoomVO();
        item.setRoomNumber(roomNumber);
        item.setBedType(bedType);
        item.setFloor(floor);
        item.setPrice(price);
        item.setRoomCount(roomCount);
        item.setRoomStyle(roomStyle);
        item.setViewType(viewType);
        item.setOptionalServices(optionalServices);
        return item;
}

InquireRoomDisplayView 클래스

  • Inquire 객체에서 전달된 InquireRoomVO 객체를 객실 종류에 따라 분기하여 출력
  • 고객이 조회한 객실의 결과값을 보여준다.
  • type에 따라 각 객실에 대한 메서드롤 호출하는데, 이 댸 RequireRoomVO 데이터 객체를 전달하지만 표현하는 부분은 생략.
public class InquireRoomDisplayView {
    public void display(int type, InquireRoomItemVO data) {
        switch(type) {
            case RoomConstValue.ROOM_TYPE_BUSINESS:
                showBusinessView(data);
                break;
            case RoomConstValue.ROOM_TYPE_FAMILY:
                showFamilyView(data);
                break;
            case RoomConstValue.ROOM_TYPE_SUITE:
                showFamilyView(data);
                break;
            default:
                break;
        }
    }

    private void showBusinessView(InquireRoomItemVO data) {
        // 중략
    }

    private void showFamilyView(InquireRoomItemVO data) {
        // 중략
    }

    private void showSuiteView(InquireRoomItemVO data) {
        // 중략
    }
}

위 코드의 문제점
1. 모든 클래스는 초기에 Inquire 클래스에서 전달된 type에 의존적.
2. 이렇게 구현된 로직은 거대한 데이터 클래스인 InquireRoomVO 클래스와 확작성이 없는 InquireRoomDisplayView 클래스에서 문제점이 나타난다.
3. 또한, 거대한 데이터 클래스를 만드는 InquireParser 역시 거대한 클래스가 되었음을 알 수 있다.

레거시 코드 개선 과정

다음과 같은 단계로 개선

  1. 연동 규약에 대한 분기를 제거하기 위한 노력.
    • Inquire 클래스의 type값이 서버 연동 모듈 외에는 영향을 주지 않도록 해야 한다.
    • type에 영향을 주는 클래스의 시작인 InquireRoomParser 클래스부터 추상화하고, 구상 클래스에서 반환하는 데이터 객체를 객실 종류에 따라 나눈다.
  2. InquireRoomDisplayView 클래스를 추상화하여 각 객실의 종류에 따른 View 클래스들은 구상 클래스로 만든다.
    • InquireRoomParser 클래스에서 반환된 데이터 객체를 인자로 받아서 처리.
  3. InquireRequsetModel 클래스의 response() 메서드는 추상화된 Parser 클래스를 인자로 받아 구상 클래스에 따른 데이터 인스턴스를 반환하게 한다.

type에 종속된 InquireRoomVO 객체 분할

  • 여기서 roomNumber, floor, price 필드와 getter, setter의 중복 코드를 제거.
  • 데이터 객체 역시 추상화
    - 데이터 객체를 추상화하면 겱국 사용하는 곳에서 Casting을 하게 되어 Object 타입으로 매개변수를 받는 것과 다를 바가 없다.
public class BusinessRoomVO {
    private String roomNumber;
    private String floor;
    private String price;
    private String bedType;

    // settters

    // getters
}

public class FmaillyRroomVO {
    private String roomNumber;
    private String floor;
    private String price;
    
    private int roomCount;
    private String roomStyle;
    private String viewType;

    // settters

    // getters
}


public class FmaillyRroomVO {
    private String roomNumber;
    private String floor;
    private String price;
    
    private int roomCount;
    private String roomStyle;
    private String viewType;

    private List<String> opionalService;

    // settters

    // getters
}

파싱 클래스 추상화

  • 데이터 객체를 반환하는 파싱 클래스
  • 파싱 클래스를 추상화, 파싱 클래스의 인스턴스가 생성되어 외부에 노출되는 인터페이스는 setResponseResult()와 parsing() 메서드이다.
  • 하지만, InquireRoomParser 클래스에서 parsing() 메서드의 반환값은 InquireRoomVO 객체.
  • 고객의 요청에 따라 BusinessRoomVO, FamilyRoomVO, SuiteRoomVO 중 하나를 반환하게 만들어야 한다.
  • 따라서 데이터 객체를 추상화하거나 Object 타입을 반환하게 해야 한다.
  • 두 방법 모두 Casting이 필요하므로 Object 타입을 반환하도록 선택.
public interface Parser {
	public void setResponseResult(String result);
    pulbic Object parsing();
}
  • 이것을 구현해서 각각에 맞게 파싱하는 구상 클래스는
    - BusinessInquireRoomParser, FamilyInquireRoomParser, SuiteInquireRoomParser.
    (각각 응답값이 정해져 있으므로 필요한 요소만 파싱하면 되고, 파싱된 데이터 결과 역시 각 역할메 맞는 데이터 객체를 반환)
public class BusinessInquireRoomParser implements Parser {
    // 중략

    @Override
    public Object parsing() {
        // 중략

        String roomNumber = itemJsonObject.getString("room_number");
        String floor = itemJsonObject.getString("floor");
        String roomNumber = itemJsonObject.getString("price");
        String bedType = itemJsonObject.getString("bed_type");
        // 중략

        BusinessRoomVO item = new BusinessRoomVO();
        item.setRoomNumber(roomNumber);
        item.setBedType(bedType);
        item.setFloor(floor);
        item.setPrice(price);
        return item;
    }

    @Override
    public void setResponseResult(String result) {
        this.result = result;
    }

    // 중략
}

public class FamilyInquireRoomParser implements Parser {
    // 중략

    @Override
    public Object parsing() {
        // 중략

        String roomNumber = itemJsonObject.getString("room_number");
        String floor = itemJsonObject.getString("floor");
        String roomNumber = itemJsonObject.getString("price");
        String bedType = itemJsonObject.getString("bed_type");

        int roomCount = itemJsonObject.getString("room_count");
        String roomStype = itemJsonObject.getString("roomStyle");
        String viewType = itemJsonObject.getString("view_type");
        // 중략

        FaimlyRoomVO item = new FaimlyRoomVO();
        item.setRoomNumber(roomNumber);        
        item.setFloor(floor);
        item.setPrice(price);

        item.setRoomCount(roomCount);
        item.setRoomStyle(roomStyle);
        item.setViewType(viewType);

        return item;
    }

    @Override
    public void setResponseResult(String result) {
        this.result = result;
    }

    // 중략
}

public class SuiteInquireRoomParser implements Parser {
    // 중략

    @Override
    public Object parsing() {
        // 중략

        String roomNumber = itemJsonObject.getString("room_number");
        String floor = itemJsonObject.getString("floor");
        String roomNumber = itemJsonObject.getString("price");
        String bedType = itemJsonObject.getString("bed_type");

        int roomCount = itemJsonObject.getString("room_count");
        String roomStype = itemJsonObject.getString("roomStyle");
        String viewType = itemJsonObject.getString("view_type");

        List<String> optionalServices = new ArrayList<String>();
        JSONArray jsonOptionalArray = itemHsonObject.getJSONArray("optional");
        
        for(OptionalJsonObject optionnalJsonObject : jsonOptionalArray) {
            // 중략
            String serviceItem = optionalJsonObject.getString("service");
            optionalServices.add(serviceItem);
        }

        // 중략

        FaimlyRoomVO item = new FaimlyRoomVO();
        item.setRoomNumber(roomNumber);        
        item.setFloor(floor);
        item.setPrice(price);

        item.setRoomCount(roomCount);
        item.setRoomStyle(roomStyle);
        item.setViewType(viewType);

        item.setOptionalService(optionalServices);
        
        return item;
    }

    @Override
    public void setResponseResult(String result) {
        this.result = result;
    }

    // 중략
}

View 클래스 개선

  • 구현해야 하는 메서드 : display()
    • 일반적인 View 클래스는 공통으로 처리해야하는 로직이 포함된 경우가 빈번하다.
    • 예를 들어, 상단, 하단을 포함하거나 동일하 UI와 UX를 가지는 컴포넌트들이 데이터를 다르게 보여줘야 하는 기능 등이 대표적
    • 인터페이스 메서드가 포함되어 구동하는 로직도 빈번히 발생
  • 추상 클래스를 활용하거나
  • 인터페이스로 추상화하고, 공통으로 구현되는 부분을 별도의 인스턴스로 포함하여 구현하는 방법.

여기에서는 dispaly() 메서드만을 사용하지만, 보편적인 View 클래스의 특성을 살려야하므로 추상 클래스를 활용하는 방법을 사용.
public abstact class AbstractInquireView {
	public abstact void display();
}
public class BusinessInquireView extends AbstractInquireView {
    // 중략

    @Override
    public void display(Object data) {
        // 중략
    }
}

public class FamilyInquireView extends AbstractInquireView {
    // 중략

    @Override
    public void display(Object data) {
        // 중략
    }
}

public class SuiteInquireView extends AbstractInquireView {
    // 중략

    @Override
    public void display(Object data) {
        // 중략
    }
}

기존 클래스의 메서드를 수정

InquireRequestModel 단위 테스트

InquireRequestModel의 requset() 메서드의 오버로딩 처리를 염두에 두고 단위 테스트 작성

public class InquireRqeusetModelTest {

    @Test
    public void testRequest_비즈니스룸_조회() {
        // Given
        Parser parser = new BusinessInquireRoomParser();
        String checkInDate = "2014-04-27";
        String checkOutDate = "2014-04-29";
        int type = 1;

        // When
        Object InquireData = InquireRequestModel.request(checkInDate, checkOutDate, parser);

        // Then
        Assert.assertEquals(BusinessRoomVO.class, inquireData.getClass());
    }
}

InquireRequestModel 클래스

InquireRequestModel의 request() 메서드를 오버로딩하여 작성

public class InquireRequestModel {
    
    // 기존 request 메서드 오버로딩
    public static InquireRoomItemVO request(String checkInDate, String checkOutDate, Parser parser) {
        // 서버에 요청
        String response = HttpUtils.postInquireRequest(makeRequestData(CheckInDate, checkOutDate, type));

        // 요청값을 파싱
        parser.setResponseResult(response);
        return parser.parsing();
    }

    // 기존 requset 메서드
    public static InquireRoomItemVO request(String checkInDate, String checkOutDate, int type) {
        // 서버에 요청
        String response = HttpUtils.postInquireRequest(makeRequestData(CheckInDate, checkOutDate, type));

        // 요청값을 파싱
        InquireRoomParser parser = new InquireRoomParser();
        parser.setResponseResult(response);

        return parser.parsing();
    }  
}

Inquire 클래스

Inquire 클래스의 메서드들을 새로 오버러딩하여 작성된 request() 메서드로 수정

public class Inquire {

    public void businessInquireButtonClicked(String checkDate, String checkOutDate) {
        // 비즈니스 룸 요청
        BusinessInquireRoomParser parser = new BusinessInquireRoomParser();
        Object data = InquireRequsetModel.request(checkInDate, checkOutDate, parser);

        // 비즈니스 룸 요청 결과 출력
        BusinessInquireView.getInstance().display(data);
    }

    public void familyInquireButtonClicked(String checkDate, String checkOutDate) {
        // 패밀리 룸 요청
        FamilyInquireRoomParser parser = new FamilyInquireRoomParser();
        Object data = InquireRequsetModel.request(checkInDate, checkOutDate, parser);

        // 패밀리 룸 요청 결과 출력
        FamilyInquireView.getInstance().display(data);
    }

    public void suiteInquireButtonClicked(String checkDate, String checkOutDate) {
        // 스위트 룸 요청
       SuiteInquireRoomParser parser = newSuiteInquireRoomParser();
        Object data = InquireRequsetModel.request(checkInDate, checkOutDate, parser);

        // 스위트 룸 요청 결과 출력
       SuiteInquireView.getInstance().display(data);
    }

}

개선된 레거시 코드

Inquire 클래스

  • 고객의 요청을 받아 조회 정보와 그에 맞는 파서와 인스턴스를 생성하여 InquireRequsetModel에 전달하고 반환된 데이터 객체를 뷰(AbstactInquireView) 인스턴스에 전달.

InquireRequestModel 클래스

  • request() 메서드로 서버에 요청하고 응답을 받아서 Inquire 클래스에서 잔달 받은 파서로 파싱을 진행하고 그 반환값을 전달.

Parser 크래스

  • 파서의 추상화 객체로 인터페이스.
  • 각각 종류별로 Parser를 구현하고, 정의된 메서드인 setResponseResult와 parsing 메서드를 구현.
  • 이 때, parsing 메서드에는 그에 맞는 VO객체를 반환.

AbstractInquireView 클래스

  • 뷰의 추상화 객체로 추상 클래스.
  • 각각 종류별로 View를 구체화하고, 추상 메서드인 display 부분을 각각 뷰에 맞게 오버라이드.
  • display에서는 Object 타입을 매개변수로 받게 되는데, 이는 각 VO객체를 받을 수 있다.

요약 및 정리

다른 시스템과의 연동 규약에 대한 코드가 구조를 지배하는 것은 확장성에 매우 취약한 모습을 보이게 된다.
그 중, type과 status 같은 종류 매개변수가 그 대표적인 예.

  • 규약에 의해 클래스들이 강하게 결합된 구조와 거대한 데이터 객체를 추상화 하여 개선함
    • 확장성이 좋아지고,
    • 경량화된 객체로 가독성과 유지보수성을 높혔다.

연동 규약에 종속된 구조를 개선하기 위한 생각의 흐름

  1. 연동 규약 요소를 처리하는 클래스를 나열.
  2. 사용자 입력에서부터 전달된 연동 규약의 요소를 제거.
  3. 중복 코드와 메서드의 책임을 판단하여 객체의 추상화를 만들어낸다.
  4. 연동하는 객체를 추상화된 객체의 매개변수로 변경하여 수정.
profile
노드 리액트 스프링 자바 등 웹개발에 관심이 많은 초보 개발자 입니다
post-custom-banner

0개의 댓글