위 책을 보면서 정리한 글입니다.
연동 규약에 너무 의존적인 기능은 연동 규약만은 바라볼 수 밖에 없다.
연동 규약에 포함된 타입 및 상태에 따라 강하게 종속된 결합 구조를 다음과 같이 개선
- 각 타입 및 상태에 맞는 클래스를 생성하여 강한 결합 구조를 제거
- 중복된 코드와 기능을 추상화로 구현하여 클래스 간 계층화를 진행
- 객체 간 연동은 인터페이스 기반으로 모듈화를 진행
연동 규약을 고려해서 설계하는 것이 무조건 나쁜 것일까
- 절대적으로 나쁜 방법은 없다. 다만, 상황에 따라 좋은 개선안이 있을 뿐, 연동 규약까지 분기문 없이 모든 구조가 전달된다면 나쁜 설계가 아니다.
- 이번 장의 문제는, 연동 규약에 따른 강한 결합 구조를 가진 클래스가 발생하고 이를 코드에서 남용한다는 점이다.
각 상황에 대한 객체를 만들거나 메서드를 만드는 것은, 객체의 종류가 추가되면 많은 부분을 수정해야 하지 않을까
- 맞는 말이다. 하지만 강한 결합구조를 클래스나 거대한 데이터 구조에서 수정하려면 예상되는 상황에 대한 도메인 로직을 파악하여 담당하는 클래스를 수정하는 작업을 거쳐야 한다.
- 사용하는 부분과 사용하지 않는 기능에 대한 구분점이 없으므로, 이 역시 파악하고 중복해서 코드를 작성해야 한다.
- 기존에 동작하던 클래스의 많은 부분을 수정하면 사이드 이펙트가 발생할 확률이 높아질 수 밖에 없다.
- 하지만, 로직을 담당하는 클래스를 추가하는 것은 기존 로직과 상관없이 구현되므로 사이드 이펙트의 가능성이 줄어든다.
- 이런 문제를 개선하기 위해서 인터페이스를 통해 객체의 계층 구조로 리팩토링 해야 한다.
인터페이스를 통한 계층 구조는 뭘 의미하는 걸까
- 객체지향의 원칙 중 하나인 DIP(Dipendency Inversion Principle)을 기반으로 리팩토링 하는 방법이다.
- DIP는 '추상화를 중심으로' 구현하는 원리라고 생각하면 된다.
- 추상화를 구체화한 객체들을 추상화 객체 중심으로 구현하면 당연히 구체화 객체들을 탄력적으로 사용하게 되고 확장도 쉬어지기 때문.
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);
}
}
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();
}
}
비즈니스 룸, 패밀리 룸, 스위트 룸에 대한 필요한 데이터를 필드로 가지고 있는 데이터 클래스, 객실마다 필요한 데이터가 약간씩 다르다.
객실 종류에 맞게 InquireRoomParser 클래스가 데이터를 입력하고, 이 데이터를 기반오르 고객 화면에 출력.
비즈니스룸 | 패밀리룸 | 스위트룸 |
---|---|---|
roomNumbe/floor/price/bedType | roomNumber/floor/price/roomCount/roomStype/viewType | roomNumber/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;
// ... 중략
}
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;
}
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 역시 거대한 클래스가 되었음을 알 수 있다.
다음과 같은 단계로 개선
- 연동 규약에 대한 분기를 제거하기 위한 노력.
- Inquire 클래스의 type값이 서버 연동 모듈 외에는 영향을 주지 않도록 해야 한다.
- type에 영향을 주는 클래스의 시작인 InquireRoomParser 클래스부터 추상화하고, 구상 클래스에서 반환하는 데이터 객체를 객실 종류에 따라 나눈다.
- InquireRoomDisplayView 클래스를 추상화하여 각 객실의 종류에 따른 View 클래스들은 구상 클래스로 만든다.
- InquireRoomParser 클래스에서 반환된 데이터 객체를 인자로 받아서 처리.
- InquireRequsetModel 클래스의 response() 메서드는 추상화된 Parser 클래스를 인자로 받아 구상 클래스에 따른 데이터 인스턴스를 반환하게 한다.
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
}
public interface Parser {
public void setResponseResult(String result);
pulbic Object parsing();
}
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;
}
// 중략
}
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의 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의 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 클래스의 메서드들을 새로 오버러딩하여 작성된 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);
}
}
다른 시스템과의 연동 규약에 대한 코드가 구조를 지배하는 것은 확장성에 매우 취약한 모습을 보이게 된다.
그 중, type과 status 같은 종류 매개변수가 그 대표적인 예.
- 연동 규약 요소를 처리하는 클래스를 나열.
- 사용자 입력에서부터 전달된 연동 규약의 요소를 제거.
- 중복 코드와 메서드의 책임을 판단하여 객체의 추상화를 만들어낸다.
- 연동하는 객체를 추상화된 객체의 매개변수로 변경하여 수정.