1. 생성자를 팩터리 메서드로 바꾸기
문제점
- new 키워드로 객체 생성시 클래스 이름이 노출됨
- 생성되는 클래스 이름을 숨기거나 상황에 맞게 인스턴스 생성이 가능하고자 할 때
- 컴파일 시간이 아닌 실행 시간에 생성되는 인스턴스가 결정되어야 할 때
리팩토링 방법
- 객체의 생성자를 private로 변경한다.
- 외부로 열려 있는 static 메서드를 만들고 해당 메서드에서 객체를 생성하도록 한다.
- 외부 호출시 new 키워드 사용을 막을 수 있다.
Before
다음과 같은 Customer 클래스와 main 함수에서 이를 호출하는 케이스를 리팩토링 해보자.
public class Customer {
private int customerType;
private String customerName;
private String customerGrade;
private int bonusPoint;
public static final int BRONZE_CUSTOMER = 0;
public static final int SILVER_CUSTOMER = 1;
public static final int GOLD_CUSTOMER = 2;
Customer(int customerType, String customerName) {
this.customerType = customerType;
this.customerName = customerName;
}
public String getCustomerGrade() {
switch (customerType) {
case BRONZE_CUSTOMER:
return "BRONZE";
case SILVER_CUSTOMER:
return "SILVER";
case GOLD_CUSTOMER:
return "GOLD";
default:
return null;
}
}
public int calcPrice(int price) {
switch (customerType) {
case BRONZE_CUSTOMER:
return price;
case SILVER_CUSTOMER:
return price - (int)(price * 0.05);
case GOLD_CUSTOMER:
return price - (int)(price * 0.1);
default:
return price;
}
}
public int calcBonusPoint(int price) {
switch (customerType) {
case BRONZE_CUSTOMER:
return bonusPoint += (price * 0.01);
case SILVER_CUSTOMER:
return bonusPoint += (price * 0.05);
case GOLD_CUSTOMER:
return bonusPoint += (price * 0.1);
default:
return price;
}
}
public String getCustomerName() {
return customerName;
}
public String toString() {
return customerName + "님의 멤버십 등급은 " + getCustomerGrade() + "입니다.";
}
}
public class Main {
public static void main(String[] args) {
Customer bronzeCustomer = new Customer(Customer.BRONZE_CUSTOMER, "Tomas");
Customer silverCustomer = new Customer(Customer.SILVER_CUSTOMER, "Alice");
Customer goldCustomer = new Customer(Customer.GOLD_CUSTOMER, "Edward");
int price = 10000;
System.out.println(bronzeCustomer);
System.out.println(bronzeCustomer.getCustomerName() + ": price :" + bronzeCustomer.calcPrice(price)
+ ": point :" + bronzeCustomer.calcBonusPoint(price));
System.out.println(silverCustomer);
System.out.println(silverCustomer.getCustomerName() + ": price :" + silverCustomer.calcPrice(price)
+ ": point :" + silverCustomer.calcBonusPoint(price));
System.out.println(goldCustomer);
System.out.println(goldCustomer.getCustomerName() + ": price :" + goldCustomer.calcPrice(price)
+ ": point :" + goldCustomer.calcBonusPoint(price));
}
}
After
기존의 new를 통해 생성되는 부분은 아래와 같이 되어야 한다.
public class Main {
public static void main(String[] args) {
Customer bronzeCustomer = BronzeCustomer.create("Tomas");
Customer silverCustomer = SilverCustomer.create("Alice");
Customer goldCustomer = GoldCustomer.create("Edward");
int price = 10000;
System.out.println(bronzeCustomer);
System.out.println(bronzeCustomer.getCustomerName() + ": price :" + bronzeCustomer.calcPrice(price)
+ ": point :" + bronzeCustomer.calcBonusPoint(price));
System.out.println(silverCustomer);
System.out.println(silverCustomer.getCustomerName() + ": price :" + silverCustomer.calcPrice(price)
+ ": point :" + silverCustomer.calcBonusPoint(price));
System.out.println(goldCustomer);
System.out.println(goldCustomer.getCustomerName() + ": price :" + goldCustomer.calcPrice(price)
+ ": point :" + goldCustomer.calcBonusPoint(price));
}
}
create() 메서드를 통해 객체를 생성하도록 바꾼다.
public class SilverCustomer extends Customer {
SilverCustomer(String customerName) {
super(customerName);
}
public static SilverCustomer create(String customerName) {
return new SilverCustomer(customerName);
}
@Override
public String getCustomerGrade() {
return "SILVER";
}
@Override
public int calcPrice(int price) {
return price - (int)(price * 0.05);
}
@Override
public int calcBonusPoint(int price) {
return bonusPoint += (price * 0.05);
}
}
public class BronzeCustomer extends Customer {
private BronzeCustomer(String customerName) {
super(customerName);
}
public static BronzeCustomer create(String customerName) {
return new BronzeCustomer(customerName);
}
public int calcPrice(int price) {
return price;
}
public String getCustomerGrade() {
return "BRONZE";
}
public int calcBonusPoint(int price) {
return bonusPoint += (price * 0.01);
}
}
public class GoldCustomer extends Customer {
private GoldCustomer(String customerName) {
super(customerName);
}
public static GoldCustomer create(String customerName) {
return new GoldCustomer(customerName);
}
@Override
public String getCustomerGrade() {
return "GOLD";
}
@Override
public int calcPrice(int price) {
return price - (int)(price * 0.1);
}
@Override
public int calcBonusPoint(int price) {
return bonusPoint += (price * 0.1);
}
}
Customer 클래스는 추상 클래스로서 객체 생성하지 못하도록 변경된다.
public abstract class Customer {
private String customerName;
protected int bonusPoint;
protected Customer(String customerName) {
this.customerName = customerName;
}
public abstract String getCustomerGrade();
public abstract int calcPrice(int price);
public abstract int calcBonusPoint(int price);
public String getCustomerName() {
return customerName;
}
public String toString() {
return customerName + "님의 멤버십 등급은 " + getCustomerGrade() + "입니다.";
}
}
2. 전략 패턴/상태 패턴 적용
문제점
- 어떤 정책 혹은 필요에 따라 여러 분기문이 생길 수 있다.
- 케이스 별로 하는 일이 다를 때 이를 분기문으로 계속 확장하는 것은 유연하지 못하고 유지보수성을 떨어뜨린다.
리팩토링 방법
- 상태패턴: 각 상태를 클래스로 만들고 수행하는 로직을 각 클래스에 작성한다.
- 전략패턴: 전략마다 클래스로 나누고 각 클래스에서 수행하는 로직을 구현한다.
-> 두 패턴 모두 상속과 오버라이딩을 활용한 패턴이다.
-> 상태 패턴은 클래스의 상태(주로 변수)에 관련하여 여러 가지 수행이 있는 경우
-> 전략 패턴은 알고리즘이 여러개 존재하는 경우
Before
플레이어의 레벨(=상태)에 따라서 각각 다른 로직을 수행하는 경우를 생각해보자. 레벨에 따라 차등되는 로직을 분기하면 다음과 같이 작성할 수 있다.
import lombok.Getter;
@Getter
public class Player {
public static final int BEGINNER_LEVEL = 1;
public static final int ADVANCED_LEVEL = 2;
public static final int SUPER_LEVEL = 3;
private int level;
public Player() {
level = BEGINNER_LEVEL;
showLevelMessage();
}
public void upgradeLevel() {
if (level == BEGINNER_LEVEL) {
level = ADVANCED_LEVEL;
} else if (level == ADVANCED_LEVEL) {
level = SUPER_LEVEL;
} else {
System.out.println("not support level");
}
showLevelMessage();
}
public void play(int count) {
run();
for (int i = 0; i < count; i++) {
jump();
}
turn();
}
public void run() {
if (level == BEGINNER_LEVEL) {
System.out.println("천천히 달립니다..");
} else if (level == ADVANCED_LEVEL) {
System.out.println("빨리 달립니다");
} else if (level == SUPER_LEVEL) {
System.out.println("엄청 빨리 달립니다.");
} else {
System.out.println("not support level");
}
}
public void jump() {
if (level == BEGINNER_LEVEL) {
System.out.println("Jump 할 줄 모르지롱.");
} else if (level == ADVANCED_LEVEL) {
System.out.println("높이 jump 합니다");
} else if (level == SUPER_LEVEL) {
System.out.println("엄청 높게 jump합니다.");
} else {
System.out.println("not support level");
}
}
public void turn() {
if (level == BEGINNER_LEVEL) {
System.out.println("Turn 할 줄 모르지롱.");
} else if (level == ADVANCED_LEVEL) {
System.out.println("Turn 할 줄 모르지롱.");
} else if (level == SUPER_LEVEL) {
System.out.println("한 바퀴 돕니다.");
} else {
System.out.println("not support level");
}
}
public void showLevelMessage() {
if (level == BEGINNER_LEVEL) {
System.out.println("***** 초보자 레벨 입니다. *****");
} else if (level == ADVANCED_LEVEL) {
System.out.println("***** 중급자 레벨 입니다. *****");
} else if (level == SUPER_LEVEL) {
System.out.println("***** 고급자 레벨 입니다. *****");
} else {
System.out.println("not support level");
}
}
}
public class MainBoard {
public static void main(String[] args) {
Player player = new Player();
player.play(1);
player.upgradeLevel();
player.play(2);
player.upgradeLevel();
player.play(3);
}
}
이와 같은 코드에서 마스터 레벨을 추가한다고 하면 어떨까?
먼저 맨 위 상수 선언에서 prviate static final int MASTER_LEVEL = 4; 를 작성하고 관련된 로직에서 모두 마스터를 추가해주어야 한다.
이를 상태 패턴을 이용해 변경해보자.
After
먼저 레벨의 추상클래스를 선언한다.
public abstract class PlayerLevel {
public abstract void run();
public abstract void jump();
public abstract void turn();
public abstract void showLevelMessage();
final public void go(int count) {
run();
for (int i = 0; i < count; i++) {
jump();
}
turn();
}
}
레벨이 할 수 있는 것들을 추상 메서드로서 정의한다. 이를 플레이어들이 상속하여 구체적인 레벨에 따라 다른 행동을 할 수 있다.
예를 들어 AdvancedLevel을 보면 다음과 같다.
public class AdvancedLevel extends PlayerLevel {
private static AdvancedLevel instance;
private AdvancedLevel() {
}
public static AdvancedLevel getInstance() {
return instance == null ? new AdvancedLevel() : instance;
}
@Override
public void run() {
System.out.println("빨리 달립니다");
}
@Override
public void jump() {
System.out.println("높이 jump 합니다");
}
@Override
public void turn() {
System.out.println("Turn 할 줄 모르지롱.");
}
@Override
public void showLevelMessage() {
System.out.println("***** 중급자 레벨 입니다. *****");
}
}
이때 Level은 계속해서 생성될 필요가 없으므로 싱글톤으로 작성한다. 상황에 따라 싱글톤으로 하면 안되는 경우도 있을 수 있다.
다른 레벨도 만들어주자.
public class BeginnerLevel extends PlayerLevel {
private static BeginnerLevel instance;
public static BeginnerLevel getInstance() {
return instance == null ? new BeginnerLevel() : instance;
}
private BeginnerLevel() {
}
@Override
public void run() {
System.out.println("천천히 달립니다..");
}
@Override
public void jump() {
System.out.println("Jump 할 줄 모르지롱.");
}
@Override
public void turn() {
System.out.println("Turn 할 줄 모르지롱.");
}
@Override
public void showLevelMessage() {
System.out.println("***** 초보자 레벨 입니다. *****");
}
}
public class SuperLevel extends PlayerLevel {
private static SuperLevel instance;
public static SuperLevel getInstance() {
return instance == null ? new SuperLevel() : instance;
}
private SuperLevel() {
}
@Override
public void run() {
System.out.println("엄청 빨리 달립니다.");
}
@Override
public void jump() {
System.out.println("엄청 높게 jump합니다.");
}
@Override
public void turn() {
System.out.println("한 바퀴 돕니다.");
}
@Override
public void showLevelMessage() {
System.out.println("***** 고급자 레벨 입니다. *****");
}
}
플레이어는 레벨을 갖도록 구현한다.
public class Player {
private PlayerLevel level;
public Player() {
level= BeginnerLevel.getInstance();
level.showLevelMessage();
}
public PlayerLevel getLevel() {
return level;
}
public void upgradeLevel(PlayerLevel level) {
this.level = level;
level.showLevelMessage();
}
public void play(int count){
level.go(count);
}
}
어떻게 레벨을 설정해주고 사용할 수 있을까? 다음과 같이 작성한다.
public class MainBoard {
public static void main(String[] args) {
Player player = new Player();
player.play(1);
AdvancedLevel aLevel = AdvancedLevel.getInstance();
player.upgradeLevel(aLevel);
player.play(2);
SuperLevel sLevel = SuperLevel.getInstance();
player.upgradeLevel(sLevel);
player.play(3);
}
}
3. 상속 대신 위임
문제점
- 코드를 재사용한다는 관점에서는 상속보다 위임의 개념이 더 적절하다.
-> 상속을 사용하면 클래스 종속성이 더 타이트해진다.
-> 상속은 반드시 써야할 때만 쓰자. (좀 더 일반적인 클래스를 보다 더 구체적으로(확장하는) 쓸 필요성이 있을 때)
- Is-A 관계와 Has-A 관계에 대해 생각해보자.
- Is-A 관계
- Is-A 관계클래스간의 상속은 종속성이 강하게 되므로, 꼭 필요한 경우 사용하는 것이 좋음
- 상속은 코드의 재사용을 위한 방법이 아님
- 일반적인 클래스와 구체적인 클래스의 관계
- Has-A 관계
- Has-A 관계사용할 클래스를 포함하여 그 기능을 해당 클래스로 위임한다.
- 클래스간의 종속성이 약함
- 이미 잘 만들어진 라이브러리나 클래스를 재사용하는 방법
-> 코드 재사용시엔 합성을 쓴다.
리팩토링 방법
- Is-A 관계가 아닌 클래스간의 상속은 Has-A로 리펙토링 한다.
Before
학생 클래스가 과목 클래스를 상속하는 경우를 생각해보자. 과목 클래스 내의 score를 사용하고 싶기 때문에 extends를 하는 경우이다.
그런데 Is-A 관계가 아니기 때문에 Subject를 상속하는 의미가 없다.
-> 단순히 코드를 많이 쓸 것 같다고 해서 상속을 하면 안 된다.
@Getter
@Setter
@NoArgsConstructor
public class Subject {
String subjectName;
int subjectCode;
int score;
public Subject(String subjectName, int subjectCode) {
this.subjectName = subjectName;
this.subjectCode = subjectCode;
}
public String toString() {
return subjectCode + ":" + subjectName;
}
}
public class Student extends Subject {
String studentName;
String studentId;
Subject majorSubject;
public Student(String studentName, String studentId, String subjectName, int subjectCode) {
majorSubject = new Subject(subjectName, subjectCode);
this.studentName = studentName;
this.studentId = studentId;
}
public void addSubject(String subjectName, int subjectCode) {
Subject subject = new Subject(subjectName, subjectCode);
subjectList.add(subject);
}
public void setSubjectScore(int subjectCode, int score) {
for (Subject subject : subjectList) {
if (subject.getSubjectCode() == subjectCode) {
subject.setScore(score);
return;
}
}
System.out.println("no subject code error");
}
public void getReport() {
int total = majorSubject.score;
System.out.println(studentName + "님의 전공과목은 " + majorSubject.getSubjectName() + "입니다.");
System.out.println(majorSubject.getSubjectName() + ":" + majorSubject.getScore());
for (Subject subject : subjectList) {
System.out.println(subject.getSubjectName() + ":" + subject.getScore());
total += subject.getScore();
}
System.out.println("Total Score : " + total);
}
}
After
상속관계를 끊고 Student 클래스에서 Subject 클래스를 포함하도록 변경하자.
public class Student {
String studentName;
String studentId;
Subject majorSubject;
ArrayList<Subject> subjectList = new ArrayList<>();
// ..
4. 에러 예외 처리
문제점
- 정상적인 처리와 에러 처리의 내용이 코드에 같이 혼재하는 경우가 있다.
-> 일관성이 없음
-> 놓치기 쉬움
- 에러 코드 상황이 점점 추가되면 어떨까?
리팩토링 방법
- 예외 상황을 Exeption 클래스로 만들어 관리하자.
Before
public class Password {
private String password;
public String getPassword(){
return password;
}
public void setPassword(String password) {
if(password == null){
System.out.println( "비밀번호는 null 일 수 없습니다");
return;
}
else if( password.length() < 5){
System.out.println( "비밀번호는 5자 이상이어야 합니다.");
return;
}
else if (password.matches("[a-zA-Z]+")){
System.out.println("비밀번호는 숫자나 특수문자를 포함해야 합니다.");
return;
}
else {
System.out.println("오류 없음");
}
this.password = password;
}
}
After
public class Password {
private String password;
public String getPassword() {
return password;
}
public void setPassword(String password) throws PasswordException {
if (password == null) {
throw new PasswordException("비밀번호는 null 일 수 없습니다");
} else if (password.length() < 5) {
throw new PasswordException("비밀번호는 5자 이상이어야 합니다.");
} else if (password.matches("[a-zA-Z]+")) {
throw new PasswordException("비밀번호는 숫자나 특수문자를 포함해야 합니다.");
}
this.password = password;
}
}
참고자료
- 패스트 캠퍼스 (박은종의 객체지향 설계를 위한 디자인패턴 with 자바)
- 리팩터링 2Edition(지은이: 마틴 파울러, 출판: 한빛 미디어)
'Lecture' 카테고리의 다른 글
자바 코드 리팩토링: 객체의 협력 관계를 디자인해보자. (0) | 2023.07.09 |
---|---|
자바 코드 리팩토링: @Builder 애노테이션을 클래스 상단에서 사용하는 것을 지양하자 (0) | 2023.07.07 |
자바 코드 리팩토링: 매직넘버, 제어 플래그, 널 문제, 분류 코드 문제, 분기문 문제 (0) | 2023.07.06 |
자바 디자인 패턴: 플라이웨이트, 프록시, 명령, 해석 패턴 (0) | 2023.07.06 |
자바 디자인 패턴: 반복자, 방문자, 연쇄책임 패턴 (0) | 2023.07.06 |