Java Refactoring
리팩터링(refactoring): 외부에서 보는 프로그램의 동작(수행하고 난 작업 결과)은 바꾸지 않고, 프로그램의 (이해, 수정, 확장을 용이하게) 내부 구조를 개선하는 방법
코드의 악취(Bad Smells in Code): 같은 코드가 여기 저기 중복(duplicated code) 되거나 메소드가 너무 길다(Long Method), 객체지향적이지 않다(Inappropriate Intimacy) 등 코드의 이해나 수정이 어렵다는 등 22가지 소개
아래 2권의 책이 Refactoring 분야의 핵심 교과서로 알려져 있음
- Martin Fowler, “Refactoring”
- Joshua Kerievsky, “Refactoring To Patterns”
적절한 이름 붙이기
1. 매직넘버를 심볼릭 정수로 치환하기 (Replace Magic Number with Symbolic Constant)
• 문제: 특별한 의미를 가진 정수나 문자열의 상수가 있고, 의미가 이해하기 힘들다.
• 해법: 이 상수를 적절한 이름을 가진 심볼릭 정수로 치환하자
▼
• 문제: 메소드의 이름을 이해하기 힘들다.
• 해법: 알기 쉬운 이름으로 바꾸자
• 문제: 식의 의미를 이해하기 어렵다.
• 해법: 이식을 초기화 할 때, 알기 쉽게 이름 앞에 임시변수를 사용한다.
• 주의: 메소드의 추출이 자주 사용된다. (?)
▼
boolean isGuest = n.equals("guest")&&p.equals("quest");
if(isAnonymous || isGuest) {...}
• 문제: 임시변수를 돌려쓰고 있다.
• 해법: 각각의 목적에 맞는 별도의 임시변수를 사용하자.
...
tmp = bottom - top
▼
int width = right - left;
...
int height = bottom - top;
임시변수 없애기
• 문제: 임시변수가 간단한 식으로 초기화 되고 후에 대입되지 않는다.
• 해법: 다른 리팩터링에 방해가 된다면 그 임시변수를 모두 식으로 치환하자.
• 주의: 이 리팩터링은 임시변수를 질문(조회, 문의)으로 치환하기의 일부로 사용되는 경우가 있다.
int width = r.getWidth();
if(width > 0){...}
▼
• 문제: 의미가 있는 식을 임시변수에 대입해서 이용하고 있기 때문에 해당 메소드에서만 이 식의 값을 활용할 수 있다.
• 해법: 이 식을 조회 메소드로 추출하자
boolean center = (_p.x == 0 && _p.y == 0);
if(center){
...
}else{
...
}
...
}
▼
if(isCenter()){
}
}
boolean isCenter(){
return _p.x == 0 && _p.y == 0;
}
• 문제: 루프에 제어 플래그가 사용되고 있어 읽기 어렵다.
• 해법 제어 플래그 대신 break, continue, return 을 사용하자.
while(flag){
if(A){
flag = false;
}else {
}
}
▼
if(A){
break;
} else {
}
}
• 문제: if-then-else가 너무 복잡하다.
• 해법: 조건부, then부, else부를 알기 쉬운 메소드 호출로 하자
score = cur * ratio * bonus;
}else{
score = cur * ratio;
}
▼
score = getInsideScore();
}else {
score = getOutsideScore();
}
• 문제: 모든 조건분기에 같은 코드가 있다.
• 해법: 이 코드를 조건분기 밖으로 이동시키자.
read();
update();
}else{
write();
update();
}
▼
read();
}else{
write();
}
update();
• 문제: if-else가 중첩이 되어 있어 정상처리가 이해하기 힘들다.
• 해법: 가드 절을 사용해서 이상처리는 빨리 return, 또는 throw 하자.
if(x<0){
result = false;
}else {
if(y < 0){
result = false;
}else{
if(width <= x){
result = false;
}else {
result = compute(x,y);
}
}
}
return result;
▼
if(y<0) return false;
if(width <= x) return false;
if(height <= y) return false;
return compute(x,y);
• 문제: 결과가 같게 되는 조건판단이 여기저기 나눠져 있다.
• 해법: 조건판단을 하나로 모아서 메소드를 추출하자.
• 주의: 메소드의 추출이 잘 되지 않으면 조건기술의 통합은 하지 않는 것이 좋다.
if (y < 0) return false;
if (width <= x) return false;
if (height <= y) return false;
▼
if(x<0 || y <0 || width <=x || height <= y) return false;
• 문제: null 체크가 너무 많다.
• 해법: null 을 표현하는 특별한 오브젝트를 도입해서 '아무것도 하지 않음' 이라는 처리를 하게 하자.
name.display();
}
▼
Name name = NullName.singletone();
name.display(); // NumName.display() --> 아무것도 표시하지 않음
• 문제: 오브젝트 타입마다 다른 동작을 하는 조건분기가 있다.
• 해법: 동작을 다형적인 메소드로 서브클래스에 이동시킨다.
• 주의: 타입코드를 서브클래스로 치환하거나 타입코드를 State/Strategy 로 치환해 둔다.
조건판단일까 예외일까
• 문제: 메소드가 오류 코드를 반환하고 있어 정상 처리와 오류 처리가 이해하기 힘들다.
• 해법: 오류코드 대신 예외를 사용하자.
if (width < 0 || height < 0){
return -1;
}else {
return width * height;
}
}
▼
if (width < 0 || height < 0){
throw new IllegalArgumentException();
}else {
return width * height;
}
}
• 문제: 예외적인 상황이 아닌데 예외를 사용하고 있다.
• 해법: 예외 대신에 조건판단을 사용하자.
container.add(item);
}catch(NullPointerException e){
}
▼
container.add(item);
}
오브젝트를 만든다.
• 문제: 만들고 싶은 인스턴스가 속한 실제 클래스를 클라이언트에 숨기고 싶다.
• 해법: Factory Method로 생성자를 치환하자.
...
}
▼
...
}
public static Shape create(int typecode){
return new Shape(typecode);
}
클래스나 인터페이스를 추출
• 문제: 하나의 클래스가 너무 많이 책임지고 있다.
• 해법: 모여있는 필드와 메소드를 찾아내서 새로운 클래스를 추출하자.
• 문제: 크게 중요하지 않은 일을 하고 있는 클래스가 있다.
• 해법: 해당 클래스를 삭제하고 내부 코드를 다른 클래스로 옮기자.
• 문제: 특정 인스턴스에ㅅ만 사용되고 있는 필드나 메소드가 있다.
• 해법: 서브클래스를 만들고 그 필드나 메소드를 옮기자
• 문제: 슈퍼클래스와 서브클래스가 대부분 같다.
• 해법: 하나의 클래스에 모으자.
• 문제: 여러 클래스에 공통점이 많다.
• 해법: 슈퍼클래스를 만들고 공통화 가능한 필드나 메소드를 그 곳에 옮기자
• 주의: 슈퍼클래스와 서브클래스 간에 IS-A 관계인지 확인한다.
• 문제: 복수의 클래스에 공통의 인터페이스(API)가 있다.
• 해법: 인터페이스를 추출하자.
<<Inteface>> Playable |
play |
stop |
pause |
수정할 수 없는 클래스에 메소드를 추가
• 문제: 이용하고 있는 서브클래스에 메소드를 추가하고 싶지만 서브클래스를 수정할 수 없다.
• 해법: 그 메소드를 클라이언트 클래스의 클래스 메소드로 추가하자
• 주의: 서브클래스의 인스턴스를 인수로 전달하면 좋다. 원래 클라이언트 클래스에 넣어야만하는 메소드가 아니므로 클라이언트 클래스의 인스턴스 필드를 직접 조작하지 않도록 클래스 메소드로 한다.
void method(){
Server server = new Server();
int result = (server를 사용한 복잡한 계산)
...
}
}
▼
void method(){
int result = computeResult(new Server())
...
}
static int computeResult(Server server){
return (server를 사용한 복잡한 계산);
}
}
• 문제: 이용하고 있는 서버클래스에 메소드를 많이 추가하고 싶지만 서버클래스를 수정할 수 없다.
• 해법: 서버클래스의 서브클래스 또 서버클래스의 랩퍼 클래스를 만들자.
캡슐화
• 문제: 필드가 public 으로 되어 있고, 외부에서 직접 액세스된다.
• 해법: 이 필드를 private로 해서 getter 메소드와 setter 메소드(액세서)를 준비하자.
public int _price;
...
}
▼
class Product {
private int _price;
public int getPrice() { return _price; }
public void setPrice(int price) { _price = price }
...
}
• 문제: 코드가 필드와 직접적으로 연관되어 있어 리팩토리하기 어렵다.
• 해법: 필드의 액세스는 자기 클래스 안에 있어도 모두 getter 메소드와 setter메소드(액세서)를 통해서 하자.
• 주의: 생성자 안에서는 필드를 직접 액세스해도 좋다.
• 문제: 변경가능한 컬렉션을 반환하는 getter 메소드가 있기 때문에 외부에서 컬렉션을 직접 조작한다.
• 해법: 변경가능한 컬렉션을 반환하지 않고 전용을 추가하거나 삭제 메소드를 별도로 제공하자.
• 주의: 컬렉션을 반환하고 싶다면 읽기 전용의 컬렉션을 반환하든지 Iterator 를 반환하게만 하자.
• 문제: 메소드의 리턴 값을 호출하는 쪽에서 다운캐스트하지 않으면 안된다.
• 해법: 다운캐스트를 메소드 내부에서 하자.
• 주의: 제네릭을 사용하면 다운캐스트 그 자체를 없애는 경우도 있다.
Map map = new HashMap();
Object getBook(String name){
return map.get(name);
}
▼
Map map = new HashMap();
Book getBook(String name){
return (Book) map.get(name);
}
(제네릭 사용)
Map<String, Book>map = new HashMap<String, Book>();
Book getBook(String name){
return map.get(name);
}
• 문제: 초기화가 끝난 후 변하지 않는 필드임에도 불구하고 setter 메소드가 있다.
• 해법: setter 메소드를 삭제하고 필드는 final 로 하자.
• 문제: 값을 조회하는 메소드가 값을 갱신하는 처리를 하고 있다.
• 해법: '값을 조회하는 메소드' 와 '값을 변경하는 메소드' 두개로 분리하자.
• 주의: 멀티스레드 프로그래밍 등에서 '조회와 갱신' 모두를 함께 실행할 필요가 있는 경우에는 두 개로 나눈 메소드가 개별적으로 외부에서 호출되지 않도록 적절히 은폐하고 동기화한 메소드를 별도로 준비해야 한다.
필드의 위치 조절
• 문제: 자신의 클래스보다도 다른 클래스로부터 많이 사용되는 필드가 있다.
• 해법: 많이 사용되는 클래스쪽으로 필드를 이동하자.
• 문제: 필드가 슈퍼클래스에 의해 선언되어 있으나 그 필드는 단지 정해진 서브클래스와 관계가 있다.
• 해법: 관계가 있는 서브클래스에 그 필드를 이동하자.
• 문제: 복수의 서브클래스에 같은 필드가 있다.
• 해법: 슈퍼클래스에 그 필드를 이동하자.
메소드의 위치 조절
• 문제: 자신의 클래스보다도 다른 클래스로부터 많이 사용되는 메소드가 있다.
• 해법: 많이 사용되는 클래스쪽으로 메소드를 이동하자.
• 문제: 메소드가 슈퍼클래스에 의해 선언되어 있으나 그 메소드는 단지 정해진 서브클래스와 관계가 있다.
• 해법: 관계가 있는 서브클래스에 그 메소드를 이동하자.
• 문제: 복수의 서브클래스에 같은 메소드가 있다.
• 해법: 슈퍼클래스에 그 메소드를 이동하자.
• 문제: 여러 개의 서브클래스에 비슷한 종류의 생성자가 있다.
• 해법: 생성자의 공통부분을 슈퍼클래스의 생성자로 이동하자.
class Player{
String name;
Device device;
}
class MusicPlayer extends Player{
MusicPlayer(String name, Device device){
this.name = name;
this.device = device;
}
}
class VideoPlayer extends Player{
VideoPlayer(String name, Device device){
this.name = name;
this.device = device;
prepareVideo();
}
}
▼
String name;
Device device;
Player(String name, Device device){
this.name = name;
this.device = device;
}
}
class MusicPlayer extends Player{
MusicPlayer(String name, Device device){
super(name, device);
}
}
class VideoPlayer extends Player{
VideoPlayer(String name, Device device){
super(name, device);
prepareVideo();
}
}
메소드 만들기
• 문제: 클래스 안에서만 사용하는 메소드임에도 불구하고 public 으로 되어 있다.
• 해법: private으로 하자.
• 문제: 하나의 메소드가 너무 길다.
• 해법: 그룹화 가능한 코드를 추출해서 이름을 붙여 메소드를 만들자.
• 문제: 일부러 이름을 붙이지 않아도 의도가 명백한 메소드가 있다.
• 해법: 호출하는 장소에 메소드의 본체를 전개하고 메소드를 지우자.
if (isPositive(value))
…
}
private bolean isPositive(int x){
return x > 0;
}
▼
• 문제: 주석으로 처리하고 싶은 전제조건이 있다.
• 해법: assertion을 사용하자.
//여기는 value 가 참이 될 것
...
}
▼
assert value > 0
...
}
• 문제: 메소드 내에서 사용되는 알고리즘을 이해하기 어렵다.
• 해법: 알고리즘을 치환하자.
for(int i = 0; i < _validUsers.length; i++){
if(_validUsers[i].equals(user)) return true;
}
return false;
}
▼
return Arrays.binarySearch(_validUsers, user) >= 0;
}
• 문제: 지역변수의 사정상 메소드의 추출이 불가능하다.
• 해법: 지역변수를 필드에 대응시킨 클래스를 만들고, 그 클래스의 인스턴스(메소드오브젝트)를 사용해서 메소드를 구현하자.
• 주의: 이 리팩터링은 꽤 트릭키한 해법이다. 적용은 신중하게
void foo(int arg){
int x = 123;
int y = 456;
int z = 790;
...
}
}
▼
void foo(int arg){
new FooMethod(this, arg, 123, 456, 789).compute();
}
}
class FooMethod {
Something something;
int arg, x, y, z;
public FooMethod(Something something, int arg, int x, int y, int z){
this.something = something;
this.arg = arg;
this.x = x;
this.y = y;
this.z = z;
}
public void compute(){
...
}
}
메소드 인수 조절
• 문제: 메소드의 내부에서 사용하고 있지 않은 인수가 있다.
• 해법: 사용하지 않는 인수를 삭제하자.
• 문제: 메소드에 필요한 정보가 부족하다.
• 해법: 인수를 증가시켜 필요한 정보를 전달하자.
• 주의: 단지 인수를 증가시켜서는 안된다. 너무 증가해야 될 듯 싶으면 인수 오브젝트의 도입을 검토할 것
return _width * _height * 1.0;
}
▼
double getArea(double ratio){
return _width * _height * ratio;
}
• 문제: 인수에 대입하고 있다.
• 해법: 인수에 대입을 없애고 임시변수를 사용하자.
if(value < 0) value = 0;
...
}
▼
int value = givenValue;
if(value < 0) value = 0;
}
• 문제: 인수의 특정 값에 대해 실행되는 코드가 분리되어 있다.
• 해법: 특정 값마다 전용의 메소드를 만들자.
• 주의: 메소드의 수를 증가시키는 대신에 인수의 수를 줄인다.
if(name == 'R') _readBuffer = buffer;
else if (name =='W') _writeBuffer = buffer;
...
}
▼
_readBuffer = buffer;
}
void setWriteBuffer(char[] buffer){
_writeBuffer = buffer;
}
• 문제: 비슷한 복수의 메소드가 값에 의존해서 다른 동작을 한다.
• 해법: 의존하는 값을 인수로 갖는 1개의 메소드로 치환하자.
• 주의: 인수의 수를 증가시키는 대신 메소드의 수가 줄고 있다.
• 문제: 메소드에 넘기는 인수를 준비하기 위해서 오브젝트에서 일일이 값을 얻지 않으면 안된다.
• 해법: 오브젝트 그 자체를 인수로 넘기자.
• 주의: 오브젝트를 넘기는 것으로 의존관계가 성립된다.
• 문제: 메소드 A를 호출할 때 언제나 메소드 X의 리턴 값을 인수로 넘기지 않으면 안된다.
• 해법: 인수를 없애고 메소드 A의 내부에서 메소드 X를 호출하자.
...
setPosition(getOrigin(), PosB);
...
void setPosition(Point origin, Point pos){
...
}
▼
...
setPosition(PosB);
...
void setPosition(Point pos){
Point origin = getOrigin();
...
}
• 문제: 언제나 모아서 전달되는 인수들이 있다.
• 해법: 인수들을 하나의 오브젝트에 모으자.
타입코드를 치환
• 문제: 타입코드가 int와 같은 기본형으로 되어 있어 형체크가 되지 않는다.
• 해법: 타입코드를 나타내는 새로운 클래스를 만들자. 또는 안전한 type의 enum을 사용하자.
public static final int TYPECODE_B = 1;
public static final int TYPECODE_C = 2;
▼
public static final ItemType B = new ItemType(1);
public static final ItemType C = new ItemType(2);
• 문제: 타입코드마다 다른 동작이 switch문으로 분기되어 있다.
• 해법: 타입코드를 서브클래스로 치환, 다형적인 메소드를 만들자.
• 문제: 타입코드마다 다른 동작이 switch문으로 분기되어 있고 동작이 변화한다.
• 해법: 타입코드를 나타내는 새로운 클래스를 만들고, 'State패턴' 또는 'Strategy 패턴'을 사용하자.
• 주의: 동작이 동적으로 변화할 때에는 타입코드를 서브클래스로 치환하기는 사용할 수 없다.
• 문제: 서브클래스들이 정수값을 반환하는 메소드밖에 가지고 있지 않다.
• 해법: 슈퍼클래스의 필드에 값을 보존시키고 서브클래스들을 삭제하자.
기존 시스템에 객체지향 도입
• 문제: 다른 의미를 가진 요소가 하나의 배열에 할당되어 있다.
• 해법: 요소를 필드에 치환하고 하나의 오브젝트로 하자.
• 문제: 이전 시스템의 레코드 구조를 다룰 필요가 있지만 객체지향적인 인터페이스(API)가 존재하지 않는다.
• 해법: 이 레코드 구조에 맞는 오브젝트를 만들자.
• 문제: 데이터가 많이 있고 그 데이터를 조작하는 함수가 별도로 준비되어 있다.
• 해법: 데이터마다에 클래스를 만든다. 데이터 값은 클래스 필드로 나타낸다. 데이터에 관련이 깊은 함수는 대응하는 클래스 메소드로 한다.
• 주의: 이것은 다른 리팩터링을 조합해서 하는 거대한 리팩터링이다.
값 오브젝트 또는 참조 오브젝트
• 문제: 하나의 값 오브젝트를 변경하면 같은 값을 가진 다른 오브젝트로 변경해야 한다.
• 해법: 같은 값을 가진 오브젝트를 하나의 참조 오브젝트로 변경하자.
• 주의: ‘값 오브젝트’란 일정이나 금액과 같이 오브젝트의 동일성보다 보존하고 있는 값 그 자체가 중요한 오브젝트이다.
• 문제: 단순하게 이뮤터블(불변)한 오브젝트를 참조 오브젝트로서 관리하는 것이 귀찮다.
• 해법: 참조 오브젝트를 값 오브젝트로 변경하자.
모델과 뷰
• 문제: 모델과 뷰가 하나의 클래스 안에 혼재되어 있다.
• 해법: 양자를 분리하고 'Observer패턴'이나 이벤트 리스너를 이용해서 동기를 얻자.
• 문제: 프리젠테이션(GUI)을 다루는 클래스가 도메인 처리(비지니스 로직)도 가지고 있다.
• 해법: 비지니스 로직을 다루는 클래스를 별도로 만든다. 동기가 필요할 때는 관찰된 데이터의 복제를 이용하자.
• 주의: 이것은 다른 리팩터링을 조합해서 하는 거대한 리팩터링이다.
상속과 위임의 전환
• 문제: IS-A 관계를 만족하지 않음에도 불구하고 상속이 사용되고 있다.
• 해법: 위임을 사용해서 상속을 치환하자.
• 문제: 위임처의 메소드 전부에 대해서 위임 메소드를 준비하고 있다.
• 해법: 위임을 하지 말고 위임처의 클래스를 슈퍼클래스로서 상속관계를 만들자.
• 주의: '상속은 최후의 무기'이므로 이 리팩터링을 적용하는 것에는 주의가 필요하다. IS-A 관계를 만족하고 있는 지를 조사한다.
위임
• 문제: 클라이언트 클래스가 서버 클래스에 있는 위임 클래스까지 직접 이용하고 있다.
• 해법: 서버 클래스에 위임 메소드를 추가하고 클라이언트 클래스로부터 위임 클래스를 숨기자.
• 문제: 위임 메소드만의 클래스가 있다.
• 해법: 중개인을 지우자.
상속
• 문제: 여러 개의 서브클래스에 ‘순서는 같고 내용이 다른 메소드’가 있다.
• 해법: 같은 순서의 부분을 Template Method로 해서 슈퍼클래스에 쓰자.
…
}
Class AppXXX extends App {
void excute() {
beforeXXX();
try {
excuteXXX();
} finally {
afterXXX();
}
}
}
Class AppYYY extends App {
void excute() {
beforeYYY();
try {
excuteYYY();
} finally {
afterYYY();
}
}
}
▼
void excute() {
doBefore();
try {
doExcute();
} finally {
doAfter();
}
}
protected abstract void doBefore();
protected abstract void doExecute();
protected abstract void doAfter();
…
}
Class AppXXX extends App {
@override void doBefore() { … }
@override void doExecute() { … }
@override void doAfter() { … }
}
Class AppYYY extends App {
@override void doBefore() { … }
@override void doExecute() { … }
@override void doAfter() { … }
}
• 문제: 하나의 클래스 계층이 여러 종류의 일을 하고 있다.
• 해법: 상속을 분할하고 필요한 일은 위임을 사용해서 이용하자.
• 문제: 많은 경우 분리를 포함하고 있는 거대한 클래스가 있다.
• 해법: 서브 클래스가 각각의 경우를 담당하는 클래스 계층을 만들자.
• 주의: 이것은 다른 리팩터링을 조합시켜서 하는 거대한 리팩터링이다.
위 내용은 Fowler의 목록을 아래 책에서 요약하여 부록으로 실린 것을,
구글링으로 검색한 페이지(http://sinihong.springnote.com/pages/768916.xhtml)에서
그 내용을 가져와 누락된 몇 가지와 일부 그림(여긴 그림이 안올라가네... -.,-;;)과 코드를 보충하여 작성함.
- 박건태역, 히로시 유키, "Java 언어로 배우는 리팩토링 입문", 한빛미디어, 2007
2012.04.17.