1. 리팩토링
1) 널 객체 도입
- 널 객체를 사용하면 '이 변수는 현재 null인가' 라는 확인을 생략할 수 있음
2) 리팩토링 카탈로그
이름 | 널 객체 도입 (Introduce Null Object) |
상황 | 객체를 다룸 |
문제 | Null 확인이 너무 많은 경우 |
해법 | Null을 나타내는 특별한 개체를 도입해 ‘아무것도 안 함’ 이라는 처리를 하자 |
결과 | 장점 : null 확인이 줄어든다. 단점 : 널 객체만큼 클래스가 늘어난다. |
2. 예제 프로그램
클래스명 | 역할 |
Person | 사람을 나타내는 클래스(이름과 메일 주소를 담는다) |
Label | 표시 가능한 문자열을 나타내는 클래스 |
Main | 동작 확인용 클래스 |
1) 리팩토링 전
public class Person {
private final Label _name;
private final Label _mail;
public Person(Label name, Label mail) {
_name = name;
_mail = mail;
}
public Person(Label name) {
this(name, null);
}
public void display() {
if (_name != null) {
_name.display();
}
if (_mail != null) {
_mail.display();
}
}
public String toString() {
String result = "[ Person:";
result += " name=";
if (_name == null) {
result += "\"(none)\"";
} else {
result += _name;
}
result += " mail=";
if (_mail == null) {
result += "\"(none)\"";
} else {
result += _mail;
}
result += " ]";
return result;
}
}
public class Label {
private final String _label;
public Label(String label) {
_label = label;
}
public void display() {
System.out.println("display: " + _label);
}
public String toString() {
return "\"" + _label + "\"";
}
}
public class Main {
public static void main(String[] args) {
Person[] people = {
new Person(new Label("Alice"), new Label("alice@example.com")),
new Person(new Label("Bobby"), new Label("bobby@example.com")),
new Person(new Label("Chris")),
};
for (Person p : people) {
System.out.println(p.toString());
p.display();
System.out.println("");
}
}
}
2) 리팩토링 실행
⑴ 널 객체 클래스 작성
① 널 객체 클래스 작성
- Label의 하위 클래스로 널 객체 틀래스를 작성한다
public class NullLabel extends Label { }
② isNull 메서드 작성
- 널 객체인지 판정하는 isNull 메서드를 작성한다.
- 기존 Label 클래스는 false를 반환하고, NullLabel 클래스는 true를 반환한다.
public class Label {
...
public boolean isNull() {
return false;
}
}
public class NullLabel extends Label {
@Override public boolean isNull() {
return true;
}
}
③ 컴파일
- 컴파일 에러가 나는데, 에러가 나는 이유는 NullLabel 클래스가 Label클래스를 상속받았는데,
Label클래스에는 기본생성자가 없기때문에 묵시적인 생성자 호출이 안된다. 따라서 직접 명시적으로 호출해야한다.
public NullLabel() {
super("(none)");
}
⑵ null 치환하기
① null을 널 객체로 치환한다.
② null 확인을 isNull 메서드 호출로 치환
③ 컴파일해서 테스트
⑶ 널 객체 클래스를 재정의해서 조건 판단 삭제하기
① isNull 메서드를 사용하는 조건 판단문 찾기
② 널 객체 클래스에서 doSomething 메서드를 오버라이드함. 이 메서드에는 <null 동작>을 작성함
public class NullLabel extends Label {
...
@Override public void display() {
}
}
③ 조건 판단 삭제
④ 컴파일해서 테스트
3) 리팩토링 후
public class Person {
private final Label _name;
private final Label _mail;
public Person(Label name, Label mail) {
_name = name;
_mail = mail;
}
public Person(Label name) {
this(name, new NullLabel());
}
public void display() {
_name.display();
_mail.display();
}
public String toString() {
return "[ Person: name=" + _name + " mail=" + _mail + " ]";
}
}
public class Label {
private final String _label;
public Label(String label) {
_label = label;
}
public void display() {
System.out.println("display: " + _label);
}
public String toString() {
return "\"" + _label + "\"";
}
public boolean isNull() {
return false;
}
}
public class NullLabel extends Label {
public NullLabel() {
super("(none)");
}
@Override public void display() {
}
@Override public boolean isNull() {
return true;
}
}
3. 한 걸음 더 나아가기
1) 팩토리 메서드 패턴
- 인스턴스 생성에 직접 클래스 명을 사용하지 않는 것
public static Label newNull() {
return new NullLabel();
}
new NullLabel() => Label.newNull()
2) 싱글톤 패턴
- 널 객체가 필요할 때마다 new NullLabel()로 인스턴스를 작성하는 것은 시간과 메모리 낭비이다.
- 따라서 싱글톤 패턴으로 실제 인스턴스를 줄이는 게 낫다.
싱글톤 패턴 : 어떤 클래스가 최초 한번만 메모리를 할당하고(Static) 그 메모리에 객체를 만들어 사용하는 디자인 패턴
3) 널 객체로 중첩 클래스 사용
- 널 객체를 도입하면 null 확인이 줄어들지만 클래스 개수가 늘어난다.
- 널 객체 클래스를 원래 클래스의 중첩 클래스(클래스안에 클래스)로 구현하면 클래스 관리가 조금 편해진다.
- 팩토리 메서드 패턴을 사용하면 NullLabel 이라는 클래스 이름은 Label 클래스 외부에 노출되지 않아도 되므로, NullLabel클래스는 private로 선언한다.
public class Label {
private final String _label;
public Label(String label) {
_label = label;
}
public void display() {
System.out.println("display: " + _label);
}
public String toString() {
return "\"" + _label + "\"";
}
public boolean isNull() {
return false;
}
// 팩토리 메서드
public static Label newNull() {
return NullLabel.getInstance();
}
// 널 객체
private static class NullLabel extends Label {
// 싱글톤
private static final NullLabel singleton = new NullLabel();
private static NullLabel getInstance() {
return singleton;
}
private NullLabel() {
super("(none)");
}
@Override public void display() {
}
@Override public boolean isNull() {
return true;
}
}
}
public class Person {
private final Label _name;
private final Label _mail;
public Person(Label name, Label mail) {
_name = name;
_mail = mail;
}
public Person(Label name) {
this(name, Label.newNull());
}
public void display() {
_name.display();
_mail.display();
}
public String toString() {
return "[ Person: name=" + _name + " mail=" + _mail + " ]";
}
}
4) null 확인이 나쁜가?
- 그건 아니지만, null 확인이 너무 많으면 널객체로 바꾸면 실수를 방지할 수 있다
5) 패턴 중독에 빠지지 않기
- 패턴을 적용할때도, 리팩토링할 때도 "지금 여기서 해결해야 하는 문제는 무엇인가" 를 의식해야한다.
- null 확인이 너무 많다면 널 객체를 도입하자!
- 클래스명을 은폐할려면 팩토리 메소드 패턴을 사용하자
- 메모리 소비량이 많으면 싱글톤 패턴을 사용하자!
위 3가지가 무조건적으로 해야하는것은 아니다.
6) 상수와 널 객체
- newNULL 이라는 메서드로 널 객체를 제공하는 게 아니라 NULL이라는 클래스 필드를 선언하는 방식도 있음
- 이렇게 하면 '널 객체는 상수의 한 종류다'라는걸 명확하게 소스코드로 표현 가능
public class Label {
public final static Label NULL = new NullLabel();
...
// 널 객체
private static class NullLabel extends Label {
private NullLabel() {
super("(none)");
}
@Override public void display() {
}
@Override public boolean isNull() {
return true;
}
}
}
7) isNull 메서드는 필요한가
- isNull 메서드는 실제 코드에 따라서 만들어도 되고.. 안만들어도 된다.
8) 기존 클래스를 수정할 수 없다면
obj.isNull() => obj instanceof Null 을 사용
- 상위 클래스를 변경할 수없으면 위 방법이 해결책이다. (마커 인터페이스)
'Major > Java' 카테고리의 다른 글
[자바로 배우는 리팩토링 입문] 6장 클래스 추출 (0) | 2022.06.02 |
---|---|
[자바로 배우는 리팩토링 입문] 5장 메서드 추출 (0) | 2022.06.01 |
[자바로 배우는 리팩토링 입문] 3장 어서션 도입 (0) | 2022.04.15 |
[자바로 배우는 리팩토링 입문] 2장 제어 플래그 삭제 (0) | 2022.04.13 |
[자바로 배우는 리팩토링 입문] 1장 매직 넘버를 기호 상수로 치환 (0) | 2022.04.13 |