위 책을 보면서 정리한 글입니다.
상속 구조는 초기 설계에 포함하여 설계하자
해결 방법 : 결합도가 높은 상속 구조는 각 기능을 객체로 추출하여 필요한 모듈에서 사용하도록 컴포지션 구조로 변경해야 한다.
상속은 자바의 강력한 기능인데, 왜 상속을 가급적 피하고 컴포지션을 사용해야 할까
- 상속은 클래스를 재사용할 수 있어 매우 편리하지만, 재사용, 수정, 확장면에서 상속보다 컴포지션이 용이하다.
- 부모 클래스가 수정되었을 때 상속받아서 구현된 클래스 중 어떤 클래스가 어느 정도 영향을 받을지를 파악하기는 어렵다.
- 같은 패키지 외에서 상속이 가능한 클래스는 지속적인 관리가 힘들어 유지보수가 어려워진다.
- 따라서, 상속을 통한 재사용은 구현한 개발자가 아니라면 사용하지 않는 것이 좋다.
- 해당 상속을 필수로 사용해야 한다면, 클래스 상속에 대한 문서를 정확하게 만들어 놓는 것이 중요.
생성만 별도로 분리하면 결제 실행은 어떻게 진행할까
- 결제 객체들은 공통의 인터페이스로 상속하면 결제 요청 객체에서는 인터페이스로 결제 실행을 처리.
- 결제 객체를 받는 요청객체에서는 어떤 객체를 반환하는지 알 필요 없이 결제만 수행 가능
별도의 생성 관리 개체를 사용하지 않고, 기존의 결제 요청 객체 내에서 결제 객체를 생성하는 메서드를 사용해도 되지 않을까
- 기존의 객체는 결제를 요청하고 응답을 처리하는 기능을 가졌는데 이는 결제 객체 생성과는 무관한 기능
- 나중에 다른곳에서 객체 생성을 재사용하는 상황이 생기면 엉뚱한 곳에서 객체 생성을 담당한다고 생각할 수 있다.
구분 | 기능 |
---|---|
정보 추출 | 1. 발송해야할 전체 번화번호 추출 2. 예약 고객 전화번호 추출 3. 문자를 발송할 전체 고객의 수 추출 4. 문자를 발송할 예약 고객의 수 반환 |
발송 관련 | 1. 예약 고객에게 문자 발송 예약 처리 2. 전체 고객에게 문자 발송 예약 처리 3. 예약 고객에게 문자 발송 4. 전체 고객에게 문자 발송 |
public class Merge {
private DBManager dbManager;
private SMSSender smsSender;
public Merge() {
dbManager = new DBManager();
smsSender = new SMSSender();
}
public List<String> getAllPhoneNum() {
// 발송해야 할 전체 전화번호 추출
return dbManager.getAllMemberPhoneNum();
}
protected String getReservationPhoneNum(int reservationNum) {
// 예약 고객 전화번호 추출
return dbManager.getReservationMemberPhoneNum(reservationNum);
}
protected int getAllSendCount() {
// 문자를 발송할 전체 고객의 수 추출
return dbManager.getAllMemberCount();
}
protected int getReservationSendCount(int reservationNum) {
// 문자를 발송할 예약 고객의 수 반환
return dbManager.getReservationMemberCount(reservationNum);
}
protected boolean reserveTargetSendSMS(String message, String phoneNum) {
// 예약 고객에게 문자 발송 예약 처리
return smsSender.reserveTargetSMS(message, phoneNum);
}
protected boolean reserveAllSendSMS(String message, List<String> phoneList) {
// 전체 고객에게 문자 발송 예약 처리
return smsSender.reserveAllSMS(message, phoneList);
}
protected boolean sendTargetSMS(String message, String phoneNum) {
// 예약 고객에게 문자 발송
return smsSender.sendTargetSMS(message, phoneNum);
}
protected boolean sendAllSMS(String message, List<String> phoneList) {
// 전체 고객에게 문자 발송
return smsSender.sendAllSMS(message, phoneList);
}
}
public class AllReservationMerge extends Merge {
public void merge(String message) {
// 전체 가입 고객의 전화번호 추출
List<String> phoneList = getAllPhoneNum();
// 문자 발송할 전체 수 추출
int allSendCnt = getAllSendCount();
// 추출한 2개의 정보를 DB에 저장
// 중략
// SMS 발송
boolean result = resrveAllSendSMS(message, phoneList);
}
}
public class AllSendMerge extends Merge {
public void merge(String message) {
// 전체 가입 고객의 전화번호 추출
List<String> phoneList = getAllPhoneNum();
// 문자 발송할 전체 수 추출
int allSendCnt = getAllSendCount();
// 추출한 2개의 정보를 DB에 저장
// 중략
// SMS 발송
boolean result = sendAllSMS(message, phoneList);
}
}
public class TargetReservationMerge extends Merge {
public void merge(String message) {
// 전체 가입 고객의 전화번호 추출
List<String> phoneList = getAllPhoneNum();
// 문자 발송할 전체 수 추출
int allSendCnt = getAllSendCount();
// 추출한 2개의 정보를 DB에 저장
// 중략
// SMS 발송
boolean result = reserveTargetSendSMS(message, phoneList);
}
}
public class TargetSendMerge extends Merge {
public void merge(String message) {
// 전체 가입 고객의 전화번호 추출
List<String> phoneList = getAllPhoneNum();
// 문자 발송할 전체 수 추출
int allSendCnt = getAllSendCount();
// 추출한 2개의 정보를 DB에 저장
// 중략
// SMS 발송
boolean result = sendTargetSMS(message, phoneList);
}
}
위 코드의 문제점
1. 비슷하거나 여러 곳에서 공통으로 사용하는 기능을 부모 클래스에서 집중하여 자식클래스에서 상속받아 사용
2. 계속 기능이 추가되면 부모 클래스가 거대화
3. 그에 따라 기능 수정의 유연성과 확장성을 어렵게 만드는 구조로 변하게 된다.
- Merge 클래스에서 전체 고객 기능 클래스 추출
- Merge 클래스에서 예약 고객 기능 클래스 추출
- 자식 클래스의 상속 구조 제거 및 추출 클래스 사용
- 자식 클래스의 공통 메서드에 해당하는 인터페이스 생성
- 추출할 기능에 대해 단위 테스트 작성
- 추출 기능에 해당하는 객체와 메서드 생성
- 기존 비즈니스 로직을 추출하여 이동
- 단위 테스트 성공 확인
public class AllMemberInfoManagerTest {
private AllMemberInfoManager allMemberInfoManager;
@Before
public void setUp() {
allMemberInfoManager = new AllMemberInfoManager();
}
@Test
public void testGetAllPhoneNum() {
// Given
// When
List<String> result = allMemberInfoManager.getAllPhoneNum();
// Then
Assert.assertNotNull(result);
}
@Test
public void testGetAllSendCount() {
// Given
// When
int result = allMemberInfoManager.getAllSendCount();
// Thean
Assert.assertNotSame(0, result);
}
}
public class AllMemberInfoManager {
private DBManager dbManager;
public AllMemberInfoManager() {
dbManager = new DBManger();
}
public List<String> getAllPhnoeNum() {
// 전체 발송할 전화번호 추출
return dbManager.getAllMemberPhoneNum();
}
public int getAllSendCount() {
// 문자 발송할 인원 추출
return dbManager.getAllMemberCount();
}
}
public class TargetMemberInfoManagerTest {
private TargetMemberInfoManager targetMemberInfoManager;
@Before
public void setUp() {
targetMemberInfoManager = new TargetMemberInfoManager();
}
@Test
public void testGetReservationPhoneNum() {
// Given
int reservationNum = 2413;
// When
List<String> result = targetMemberInfoManager.getReservationPhoneNum(reservationNum);
// Then
Assert.assertNotNull(result);
}
@Test
public void testGetReservationSendCount() {
// Given
int reservationNum = 2413;
// When
int result = targetMemberInfoManager.getReservationSendCount(reservationNum);
// Thean
Assert.assertNotSame(1, result);
}
}
public class TargetMemberInfoManager {
private DBManager dbManager;
public TargetMemberInfoManager() {
dbManager = new DBManger();
}
public List<String> getReservationPhoneNum(int reservationNum) {
// 예약 고객 전화번호 정보 추출
return dbManager.getReservationPhoneNum(reservationNum);
}
public int getReservationSendCount(int reservationNum) {
// 문자 발송할 인원 추출
return dbManager.getAllMemberCount(reservationNum);
}
}
// 이게 정확한지는 잘 모르겠다.. 책에서는 reservationNum에 관한 내용이 빠져있었다.
// 이렇게 되면 reservationNum을 필요하지 않아도 AllReservationMerge나, AllSendMerge에서는 구현해야 하고
// 또한, TargetReservationMerge, TargetSendMerge에서는 필요하지 않은 merge(message) 메서드를 구현해야 한다.
public interface Merge {
public void merge(String message);
public void merge(String message, int reservationNum);
}
// 전체 고객 예약 발송
public class AllReservationnMerge implements Merge {
@Override
public void merge(String message) {
AllMemberInfoManager allMemberInfoManager = new AllMemberInfoManager();
SMSSender smsSender = new SMSSender();
// 전체 고객 전화번호 정보 추출
List<String> phoneList = allMemberInfoManager.getAllPhoneNum();
// 전체 발송 수량 추출
int allSendCnt = allMemberInfoManager.getAllSendCount();
// 추출한 2개 정보 DB 저장
// 중략
// SMS 발송
boolean result = smsSender.reservationAllSMS(message, phoneList);
}
@Override
public void merge(String message, int reservationNum) {
//
}
}
// 전체 고객 즉시 발송
public class AllSendMerge implements Merge {
@Override
public void merge(String message) {
AllMemberInfoManager allMemberInfoManager = new AllMemberInfoManager();
SMSender smsSender = new SMSSender();
// 전체 고객의 전화번호 정보 추출
List<String> phoneList = allMemberInfoManager.getAllPhoneNum();
// 전체 발송 수량 추출
int allSendCnt = allMemberInfoManager.getAllSendCount();
// 추출한 2개 정보 DB 저장
// 중략
// SMS 발송
boolean result = smsSender.sendAllSMS(message, phoneList);
}
@Override
public void merge(String message, int reservationNum) {
//
}
}
// 예약 고객 예약 발송
public class TargetReservationMerge implements ReservationMerge {
@Override
public void merge(String message) {
//
}
@Override
public void merge(String message, int reservationNum) {
TargetMemberInfoManager targetMemberInfoManager = new TargetMemberInfoManager();
SMSender smsSender = new SMSSender();
// 전체 고객의 전화번호 정보 추출
List<String> phoneList = targetMemberInfoManager.getReservationPhoneNum(reservationNum);
// 전체 발송 수량 추출
int allSendCnt = targetMemberInfoManager.getReservationSendCount(reservationNum);
// 추출한 2개 정보 DB 저장
// 중략
// SMS 발송
boolean result = smsSender.reserveTargetSMS(message, phoneList);
}
}
// 예약 고객 즉시 발송
public class TargetReservationMerge implements ReservationMerge {
@Override
public void merge(String message) {
//
}
@Override
public void merge(String message, int reservationNum) {
TargetMemberInfoManager targetMemberInfoManager = new TargetMemberInfoManager();
SMSender smsSender = new SMSSender();
// 전체 고객의 전화번호 정보 추출
List<String> phoneList = targetMemberInfoManager.getReservationPhoneNum(reservationNum);
// 전체 발송 수량 추출
int allSendCnt = targetMemberInfoManager.getReservationSendCount(reservationNum);
// 추출한 2개 정보 DB 저장
// 중략
// SMS 발송
boolean result = smsSender.sendTargetSMS(message, phoneList);
}
}
기능 재사용의 강력함, 단숨함에 의해 상속을 남용하면 프로젝트를 수정하기 어려워진다.
- 부모와 자식 관계인지 확인
- 부모 클래스가 자식 클래스에서 사용하는 공통 기능을 모두 담고 잇는 거대 클래스인지 확인
- 부모 클래스의 기능을 분류하여 클래스로 추출
- 컴포넌트에서 원하는 기능의 클래스를 조합하여 사용
- 인터페이스를 추출하여 클라이언트에 일관된 기능 호출 방식을 제공하고 컴포넌트와의 결합도를 낮춘다