위 책을 보면서 정리한 글입니다.
클래스 내에서 관계가 독립적인 메서드는 시간이 지날수록 효율적으로 재사용되지 않는다.
기능만을 반복하여 사용할 때 가장 좋은 방법은 독립적인 기능을 떼어 내어 모듈화하는 것.
↪ 모듈화하는 방법 중 하나는 static 메서드로 전환하여 참조 관계가 없는 독립된 기능을 만드는 것.
불필요하게 중복된 기능을 개선하는 방법으로 보통, 클래스 추출을 통해 기능을 재사용하도록 변경하는 것을 생각하게 된다.
↪ 보통은 클래스 추출로 해결이 되는 경우가 많으나, 효율적으로 개선이 되지 않는 경우가 존재.
현재 문제 상황을 가정해보자면,
공통된 로직을 가지면서, 각 위치에 있는 객체 내 멤버 변수나 다른 메서드와의 참조 관계가 전혀 없는 독립된 기능
이 경우에, 클래스 추출 후 사용할 때마다 인스턴스를 생성하는 것이 불필요할 수 있다.
따라서 static 메서드로 추출하여 효율적으로 공통 로직의 기능을 개선.
static 패턴이란
- 자바에서 static으로 선언되면, 프로그램이 시작될 때 Static Area 메모리 영역에 올라 프로그램이 ㅈ종료될 때 사라진다.
- 객체를 생성하지 않고 변수나 메서드를 사용 가능
- static 메서드는 non static보다 먼저 생성되므로 non satic 필드나 메서드에는 접근 불가.
static으로 선언할 수 있는 대상
- static 변수
- 클래스 객체의 생성 없이 모든 객체가 공유 가능- static 메서드
- 객체 내용에 의존적이지 않은 하나의 작업을 수행. static으로 선언된 메서드는 오버라이딩/상속받은 클래스에서 사용불가- static 초기화 블록
- 클래스에서 static {} 형태로 지정 가능. 이렇게 지정된 블록은 클래스가 최초 로드될 때 한 번만 수행- static import
- 다른 클래스에 있는 static 멤버들을 불러오기 위해 사용.
- import static PackageName.ClassName
- import static PackageName.ClassName.merberName (클래스명의 선언 없이 static 멤버만 선언해서 사용 가능)
static 메서드로 추출하기 위한 조건
1. 위치한 객체 내에서 독립적으로 사용되는 경우
2. 독립된 로직 사용 때문에 인스턴스 생성이 불필요한 경우
3. 공통 메서드로 추출할 때 사용 규약이 명확해야 하는 경우
public interface Payment {
/*
* 결제
*/
public void pay();
/*
* 결제 취소
*/
public void cancel();
}
/* 신용카드 결제 담당
*
* pay() 메서드를 사용하여 신용카드/실시간 계좌이체/마일리지 결제를 진행하고,
* totalCharge() 메서드로 객실 종류, 투숙 기간에 따른
* 요금을 반환받아 결제 처리
*/
public class CreditCared implements Payment {
private final int INSTALLMENT = 1; // 할부
private final int LUMP_SUM = 2; // 일시불
private final int STANDARAD = 1;
private final int DELUXE = 2;
private final int SUITE = 3;
private final int STANDARD_CHARGE = 200000;
private final int DELUXE_CHARGE = 300000;
private final int SUITE_CHARGE = 400000;
private String cardNumber;
private String cvc;
private String lastYear;
private String lastMonth;
private int paymentType;
private String paymentCode;
private String startDate;
private String endDate;
private int roomType;
@Override
public void pay() {
float charge = totalCharge(roomType);
if(!validateCardNumber(cardNumber)) {
// 신용카드 번호 오류
}
if(creditCardPayment(cardNumber, cvc, lastYear, lastMonth, paymentType, charge)) {
// 신용카드 결제 성공
} else {
// 신용카드 결제 실패
}
}
@Override
public void cancel() {
// 신용카드 결제 취소
if(paymentCancel(cardNumber, paymentCode)) {
// 신용카드 결제 취소 성공
} else {
// 신용카드 결제 취소 실패
}
}
// 신용카드 결제 취소
public CreditCard(String cardNumber, String paymentCode) {
this.cardNumer = cardNumber;
this.paymentCode = paymentCode;
}
// 신용카드 결제
public CreditCard(String cardNumber, String cvc, String lastYear, String lastMongth, int paymentCode, String startDate, String endDate, int roomType) {
this.cardNumber = cardNumber;
this.cvc = cvc;
this.lastYear = lastYear;
this.lastMonth = lastMongth;
this.paymentCode = paymentCode;
this.startDate = startDate;
this.endDate = endDate;
this.roomType = roomType;
}
}
/* 실시간 계좌이체 결제 담당
*
* pay() 메서드를 사용하여 신용카드/실시간 계좌이체/마일리지 결제를 진행하고,
* totalCharge() 메서드로 객실 종류, 투숙 기간에 따른
* 요금을 반환받아 결제 처리
*/
public class AccountTransfer implements Payment {
private final int STANDARAD = 1;
private final int DELUXE = 2;
private final int SUITE = 3;
private final int STANDARD_CHARGE = 200000;
private final int DELUXE_CHARGE = 300000;
private final int SUITE_CHARGE = 400000;
private String bankName;
private String accountNumber;
private String paymentCode;
private String startDate;
private String endDate;
private int roomType;
@Override
public void pay() {
float charge = totalCharge(roomType);
if(!validateAccountNumber(bankName, accountNumber)) {
// 계좌번호 오류
}
if(AccountTransferPayment(bankName, accountNumber, paymentType, charge)) {
// 계좌이체 성공
} else {
// 계좌이체 오류
}
}
@Override
public void cancel() {
if(paymentCancel(bankName, accountNumber, paymentCode)) {
// 계좌이체 취소
} else {
// 계좌이체 취소 실패
}
}
private boolean AccountTransferPayment(String bankName, String accountNumber, String paymentCode, float charge) {
return true;
}
private boolean validateAccountNumber(String namkName2, String accountNumber2) {
return true;
}
public AccountTransfer(String cardNumber, String paymentCode) {
this.cardNumer = cardNumber;
this.paymentCode = paymentCode;
}
public AccountTransfer(String bankName, String accountNumber, String startDate, String endDate, int roomType) {
this.bankName = bankName;
this.accountNumber = accountNumber;
this.startDate = startDate;
this.endDate = endDate;
this.roomType = roomType;
}
public AccountTransfer(String bankName, String accountNumber, String paymentCode) {
this.bankName = bankName;
this.accountNumber = accountNumber;
this.paymentCode = paymentCode;
}
}
/* 마일리지 결제 담당
*
* pay() 메서드를 사용하여 신용카드/실시간 계좌이체/마일리지 결제를 진행하고,
* totalCharge() 메서드로 객실 종류, 투숙 기간에 따른
* 요금을 반환받아 결제 처리
*/
public class Mileage implements Payment {
private final int STANDARAD = 1;
private final int DELUXE = 2;
private final int SUITE = 3;
private final int STANDARD_CHARGE = 200000;
private final int DELUXE_CHARGE = 300000;
private final int SUITE_CHARGE = 400000;
private String userId;
private String paymentCode;
private String startDate;
private String endDate;
private int roomType;
@Override
public void pay() {
float charge = totalCharge(roomType);
// 사용자 마일리지 조회
int mileage = getUserMileage(userId);
if(MileagePayment(mileage, userId, charge)) {
// 마일리지 결제 성공
} else {
// 마일리지 결제 오류
}
}
@Override
public void cancel() {
if(paymentCancel(userId, paymentCode)) {
// 마일리지 결제 취소
} else {
// 마일리지 결제 취소 실패
}
}
private boolean MileagePayment(String userId, String accountNumber, String paymentCode, float charge) {
return true;
}
private boolean validateAccountNumber(String namkName2, String accountNumber2) {
return true;
}
public Mileage(String userId, String startDate, String endDate, int roomType) {
this.userId = userId;
this.startDate = startDate;
this.endDate = endDate;
this.roomType = roomType;
}
public Mileage(String userId, String paymentCode) {
this.userId = userId;
this.paymentCode = paymentCode;
}
}
private float totalCharge(int roomType) {
float totalCharge = 0.0f;
List<String> period = getPriod(startDate, endDate);
for(String data : period) {
float charge = 0.0f;
float weigh = getDateCharge(date);
if(roomType == STANDARD) {
charge = STANDARD_CHARGE * weigh;
} else if(roomType == DELUXE) {
charge = DELUXE_CHARGE * weigh;
} else if(roomType == SUITE) {
charge = SUITE_CHARGE * weigh;
}
totalCharge += charge;
}
DecimalFormat format = new DecimalFormat(".");
return Float.parseFloat(format.format(totalCharge));
}
/*
* 일자별 요금계산
*/
private float getDateCharge(String date) {
// 성수기: 기본요금 1.2배
// 주말: 기본요금 1.3배
List<String> weekend = new ArrayList<String>();
weekend.add("Sat");
weekend.add("Sun");
List<String> peakSeason = new ArrayLst<String>();
peakSeason.add("August");
peakSeason.add("September");
float weight = 0f;
if(weekend.contains(getTodayWeek(date))) {
weight += 1.3f;
} else {
weight += 1.0f;
}
if(weekend.contains(getTodayMonth(date))) {
weight += 1.3f;
} else {
weight += 1.0f;
}
DecimalFormat format = new DecimalFormat(".##");
return Float.parseFloat(format.format(weight));
}
/*
* 해당일의 달을 반환
*/
private String getTodayMonth(String date) {
DateTimeFormatter formatter = DateTimeFormatter.forPattern("yyyyMMdd");
DateTime dt = formatter.parseDateTime(date);
return dt.monthOfYer().getAsText(Locale.US);
}
/*
* 해당일의 요일을 반환
*/
private List<String> getPriod(String startDate, String endDate) {
List<String> period = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyMMdd");
DateTime startDt = formatter.parseDateTime(startDate);
DateTime endDt = formatter.parseDateTime(endDate);
int betweenDay = Days.daysBetween(startDt.withTimeAsStartOfDay(), endDt.withTimeAtStartOfDay()).getDays();
for(int i=0; i<=betweenDay; i++) {
period.add(formatter.print(startDt.plusDays(i)));
}
return period;
}
모든 결제 클래스는 결제할 때 요금을 계산하는 totalCharge() 메서드를 이용한다.
객실 종류, 투숙 기간에 따른 성수기와, 비성수기, 평일, 주말을 체크하여 총 결제 금액을 반환
각 결제 클래스의 기능은 포함한 멤버 변수에 의존하여 처리된다.
각 결제 클래스에서 공통으로 사용하는 요금 계산 기능인 totalCharge() 메서드와 해당 메서드가 호출하는 메서드들은 startDate, endDate 멤버 변수에 의존
public class Charge {
final static int STANDARD = 1;
final static int DELUXE = 2;
final static int SUITE = 3;
final static int STANDARD_CHARGE = 1;
final static int DELUXE_CHARGE = 1;
final static int SUITE_CHARGE = 1;
public static float totalCharge(String startDate, String endDate, int roomType) {
float totalCharge = 0.0f;
List<String> period = getPriod(startDate, endDate);
for(String data : period) {
float charge = 0.0f;
float weigh = getDateCharge(date);
if(roomType == STANDARD) {
charge = STANDARD_CHARGE * weigh;
} else if(roomType == DELUXE) {
charge = DELUXE_CHARGE * weigh;
} else if(roomType == SUITE) {
charge = SUITE_CHARGE * weigh;
}
totalCharge += charge;
}
DecimalFormat format = new DecimalFormat(".");
return Float.parseFloat(format.format(totalCharge));
}
/*
* 일자별 요금계산
*/
public static float getDateCharge(String date) {
// 성수기: 기본요금 1.2배
// 주말: 기본요금 1.3배
List<String> weekend = new ArrayList<String>();
weekend.add("Sat");
weekend.add("Sun");
List<String> peakSeason = new ArrayLst<String>();
peakSeason.add("August");
peakSeason.add("September");
float weight = 0f;
if(weekend.contains(getTodayWeek(date))) {
weight += 1.3f;
} else {
weight += 1.0f;
}
if(weekend.contains(getTodayMonth(date))) {
weight += 1.3f;
} else {
weight += 1.0f;
}
DecimalFormat format = new DecimalFormat(".##");
return Float.parseFloat(format.format(weight));
}
/*
* 해당일의 달을 반환
*/
public static String getTodayMonth(String date) {
DateTimeFormatter formatter = DateTimeFormatter.forPattern("yyyyMMdd");
DateTime dt = formatter.parseDateTime(date);
return dt.monthOfYer().getAsText(Locale.US);
}
/*
* 해당일의 요일을 반환
*/
public static List<String> getPriod(String startDate, String endDate) {
List<String> period = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyMMdd");
DateTime startDt = formatter.parseDateTime(startDate);
DateTime endDt = formatter.parseDateTime(endDate);
int betweenDay = Days.daysBetween(startDt.withTimeAsStartOfDay(), endDt.withTimeAtStartOfDay()).getDays();
for(int i=0; i<=betweenDay; i++) {
period.add(formatter.print(startDt.plusDays(i)));
}
return period;
}
}
public class ChargeTest extends TestCase {
@Test
public void testGetTotalCharge_기간별_호텔요금_계산() {
//Given
String startDate = "20140719";
String endDate = "20140721";
//When
float charge = Charge.totalCharge(startDate, endDate, 1);
//Then
assertEquals("19/20/21", 720000f, charge);
}
@Test
public void testGetDateCharge_해당_일에_대한_가중치_요금계산_주말() {
//Given
String date ="20240719";
//When
float weight = Charge.getDateCharge(date);
//Then
assertEuqals("비성수기_주말", 1.3f, weight);
}
@Test
public void testGetDateCharge_해당_일에_대한_가중치_요금계산_성수기_주말() {
//Given
String date = "20140816";
//When
float weight = Charge.getDateCharge(date);
//Then
assertEquals("성수기_주말", 1.56f, weight);
}
@Test
public void testGetTodayMonth_날짜를_입력받아_해당_월을_반환() {
//Given
String date = "20140719";
//When
String month = Charge.getTodayMonth(date);
//Then
assertEquals("July", month);
}
@Test
public void testGetTodayMonth_날짜를_입력받아_해당_요일을_반환() {
//Given
String date = "20140719";
//When
String month = Charge.getTodayWekk(date);
//Then
assertEqulas("Sat", month);
}
@Test
public void testGetPeriod_날짜사이의_일자를_리턴() {
//Given
String startDate = "20140719";
String endDate = "20140725";
//When
List<String> betweenDay = Charge.getPriod(startDate, endDate);
//Then
assertEquals(7, betweenDay.size());
}
}
public class CreditCared implements Payment {
private final int INSTALLMENT = 1; // 할부
private final int LUMP_SUM = 2; // 일시불
private final int STANDARAD = 1;
private String cardNumber;
private String cvc;
private String lastYear;
private String lastMonth;
private int paymentType;
private String paymentCode;
private String startDate;
private String endDate;
private int roomType;
@Override
public void pay() {
float charge = Charge.totalCharge(roomType);
// 중략...
}
// 중략...
}
static으로 변경된 메서드는 독립적으로 수행 가능하므로 각 메서드에 대해서 테스트를 진행 가능하다.
단위 기능별로 테스트케이스를 통해 기능 변경이 발생할 때에도 사이트 이펙트를 방지 가능하며, 오류가 발생해도 수정하기 편하다.
레거시 코드에서 중복된 코드는 생산성 저하를 일으키는 원인이므로 서둘러 개선해야 한다.
static 메서드를 활용한 레거시 코드 개선은 중복 코드 개선 방법의 하나이다. 중복 코드 개선에서 주로 사용하는 방법은 메서드 추출이지만, static 메서드의 추출은 멤버 메서드의 추출과는 다른 상황으로 판단해야 한다.
단순히, 메서드 추출이 아닌 책임과 역할을 분리해야 하는 경우가 아닌지를 판단하고 클래스의 생성과 함께 메서드 추출로 개선해야 한다.
책임과 역할이 좀더 명확히 분리되고, 기능 오류가 발생했을 때 빠르게 문제점을 찾아낼 수있으며 관리 지점이 줄어들어 수정에 대한 부담감이 줄어든다.
이는 결국, 유지보수가 쉬워지고, 모듈화로 인한 재사용성 역시 개선됨을 의미.