본문 바로가기
Book

[독서 기록] 자바로 배우는 리팩토링 입문

by Renechoi 2022. 11. 22.
 
자바로 배우는 리팩토링 입문
프로그램은 계획 없이 수정하거나 제대로 살펴보지 않고 기능을 추가하면 점점 읽기도, 수정하기도, 디버깅하기도 어려운 상태가 된다. 마치 건강을 관리하지 않은 사람처럼 상태가 엉망진창이 된다. 시간을 들여 운동과 식사를 관리해서 체질을 개선하듯이 계속 리팩토링하면 버그를 늘리지 않으면서도 깔끔한 코드로 프로그램 체질을 개선할 수 있다. 이 책은 자바로 된 샘플 코드를 하나씩 실습하며 프로그래머라면 꼭 알아야 할 리팩토링 기법을 쉽게 배울 수 있는 입문서다. 또한 리팩토링 전과 후 프로그램을 비교해서 분석한 내용을 일목요연하게 보여주기 때문에 프로그램이 어떻게 달라졌는지를 누구나 쉽게 이해할 수 있다. 그리고 각 장 뒤에는 중요 포인트를 재확인하고 프로그램이 풍기는 악취를 탐지하는 연습을 할 수 있게 연습문제가 있다.
저자
유키 히로시
출판
길벗
출판일
2017.10.31

자바로 배우는 리팩토링 입문, 유키 히로시 지음, 서수환 옮김, 길벗 출판 

 


리팩토링이란 외부에서 보는 프로그램 동작은 바꾸지 않고 프로그램의 내부 구조를 개선하는 것입니다.

- 28p

 

 

31p 

 

 

 

소스 코드를 보다가 이 말이 입에서 튀어나오면 '리팩토링이 필요하겠구나'라고 생각하면 됩니다.

- 겹치잖아!

- 너무 길어!

- 너무 많아!

- 이름이 안 맞잖아!

- 너무 공개적이잖아!

- 객체 지향답지 않아!

 

- 34p 

 

 

 

스텝 바이 스텝 : 두 가지 수정을 한꺼번에 하지 않기 

스텝 바이 스텝 : 되돌리기 쉽게 하기

- 39p 

 

 

실제 리팩토링은 설계 개선으로도 볼 수 있습니다.

 - 43p

 


 

매직 넘버를 기호 상수로 치환

 

 

상수 의존 관계 

 

public static final int MAX_INPUT_LENGTH = 100; 

public static final int WORK_AREA_LENGTH = MAX_INPUT_LENGTH * 2; 

 

- 55p 

 

 

명령어를 나타내는 기호 상수로 WALK, STOP, JUMP라는 enum을 선언합니다. 

 

public class Robot {
	private final String _name;
    public enum Command {
    	WALK,
        STOP,
        JUMP
    };

- 60p 

 

 


제어 플래그 삭제 

 

 

제어 플래그 할당을 break나 return으로 바꿔 쓰면 프로그램 가독성이 좋아지기도 합니다. 그 이유는 break나 return을 본 순간 이후에 오는 코드를 읽지 않아도 되는 경우가 많기 때문입니다. 

- 79p

 

 

제어 플래그가 public 인스턴스 필드라면 더욱 문제입니다. 제어 플래그를 변경하는 코드가 프로그램 전체에 등장할 가능성이 있기 때문입니다.

- 80p 

 

 

정규 표현식 패키지 사용 

 

private Map<String,String> _map = new HashMap<String,String>();
private static Pattern _pattern = Pattern.compile("([^=]+)=(.*)");
public SimpleDatabase(Reader r) throws IOException {
    BufferedReader reader = new BufferedReader(r);
    while (true) {
        String line = reader.readLine();
        if (line == null) {
            break;
        }
        Matcher matcher = _pattern.matcher(line);
        if (matcher.matches()) {
            String key = matcher.group(1);
            String value = matcher.group(2);
            _map.put(key, value);
        }

 

패턴을 Pattern.compile 메서드로 컴파일한 후 Matcher 클래스로 패턴이 일치하는지 확인합니다. 

-83p 

 


 

어서션 도입 

 

예를 들어 프로그램의 어떤 부분에서 변수 value 값이 참이어야 한다고 합시다. 만약 value 값이 참이 아니라면 프로그래머의 의도와는 다른 동작이 됩니다. 자바에서는 'value' 값이 참이어야 한다'라는 건 다음과 같이 작성합니다. 

 

assert value > 0; 

 

- 88p 

 


널 객체 도입 

 

 

1) 널 객체 클래스 작성 

 

public class NullLabel extends Label {
}

2) isNull 메서드 작성 

 

public class Label {
	...
    	public boolean isNull() {
        	return false;
        }
   }

 

- 114p 


public static Label newNull() {

 	return new Nullable();
}

이렇게 하고 나면 

 

new Nullable()

 

위의 표현식 대신에 다음과 같은 코드를 작성할 수 있습니다. 

 

Label.newNull()

 

...

 

팩터리 메서드 패턴과 싱글톤 패턴을 조합해서 NullLabel 클래스를 Label 클래스 안에 중첩 클래스로 구현한 코드 

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;
        }
    }
}

 

 

- 120 ~ 122p

 

 

 


메서드 추출 

 

역 리팩토링 

 

메서드 추출 <-> 메서드 인라인화 

- 140p 

 

클래스 추출 

 

불변 인터페이스라는 방법을 사용하면 일반 인스턴스 내용을 읽기 전용으로 만들 수 있습니다. 

 

public interface ImmutableAuthor {
    public String getName();
    public String getMail();
}

- 163p 

 

저자 정보를 변경하면 곤란한 상황일 경우에는 Author 가 아닌 Immutable Author 객체로 _author 필드값을 넘깁니다. 

 

public class Book {
	...
    private Author _author;
    ...
    public ImmutableAuthor getAuthor() {
    	return _author;
    }

 

- 163p

 


분류 코드를 클래스로 치환 

 

public class Item {
    public static final int TYPECODE_BOOK = 0;
    public static final int TYPECODE_DVD = 1;
    public static final int TYPECODE_SOFTWARE = 2;

    private final int _typecode;
    private final String _title;
    private final int _price;

 

... 상품 분류 코드를 가리킵니다. 

 

- 174p 

 

 

public class ItemType {
	public static final ItemType BOOK = new ItemType(0);
    public static final ItemType DVD = new ItemType(1);
    public static ...

이건 int 타입이 아닌 ItemType입니다. BOOK, DVD, SOFTWARE라는 인스턴스가 분류 코드를 대신합니다. 

 

private ItemType(int typecode) {
	_typecode = typecode;
}

 

ItemType 클래스 인스턴스에서 분류 코드를 얻는 게터 메서드를 만듭니다.

 

- 177p

 

 

분류 코드를 하위 클래스로 치환 

 

public static void main(String[] args) {
    Shape line = Shape.createShape(Shape.TYPECODE_LINE, 0, 0, 100, 200);
    Shape rectangle = Shape.createShape(Shape.TYPECODE_RECTANGLE, 10, 20, 30, 40);
    Shape oval = Shape.createShape(Shape.TYPECODE_OVAL, 100, 200, 300, 400);

    Shape[] shape = {
        line, rectangle, oval
    };

    for (Shape s : shape) {
        s.draw();
    }

 

 


public static Shape createShape(int typecode, int startx, int starty, int endx, int endy) {
    switch (typecode) {
    case TYPECODE_LINE:
        return new ShapeLine(startx, starty, endx, endy);
    case TYPECODE_RECTANGLE:
        return new ShapeRectangle(startx, starty, endx, endy);
    case TYPECODE_OVAL:
        return new ShapeOval(startx, starty, endx, endy);
    default:
        throw new IllegalArgumentException("typecode = " + typecode);
    }
}

 

- 202 ~ 203p 

 

 

switch 문과 instanceof 연산자가 풍기는 악취 

- 203p 

 

 

팩토리 메서드로 switch 문을 없애기 위해 좀 더 간단한 방법을 생각해 봅시다.

 

createShape라는 팩토리 메서드 하나로 다 하려고 하지 말고, 처음부터 클래스에 따른 팩토리 메서드를 다음과 같이 만들면 됩니다. 

 

ShapeLine -> createShapeLine 

ShapeRectangle -> createShapeRectangle

ShapeOval -> createShapeOval 

 


public static ShapeLine createShapeLine(int startx, int starty, int endx, int endy) {
    return new ShapeLine(startx, starty, endx, endy);
}

 

- 206p 

 

 


 

분류 코드를 상태/전략 패턴으로 치환 

 

public class Logger {
    public static final int STATE_STOPPED = 0;
    public static final int STATE_LOGGING = 1;
    private int _state;
    public Logger() {
        _state = STATE_STOPPED;
    }
    public void start() {
        switch (_state) {
        case STATE_STOPPED:

=> 분류 코드를 상태/전략 패턴으로 치환 

 

int로 구현한 분류 코드를 상태 객체로 치환 

 

public abstarc class State {
	public abstrac int getTypeCode();
}

 

public class StateStopped extends State {
	@Override public int getTypeCode() {
    	return Logger.STATE_STOPPED;
    }
}

 

- 216p 

 

 

public class Logger {
    private enum State {
        STOPPED {
            @Override public void start() {
                System.out.println("** START LOGGING **");
            }
            @Override public void stop() {
                /* 아무것도 하지 않음 */
            }
            @Override public void log(String info) {
                System.out.println("Ignoring: " + info);
            }
        },

        LOGGING {
            @Override public void start() {
                /* 아무것도 하지 않음 */
            }
            @Override public void stop() {
                System.out.println("** STOP LOGGING **");
            }
            @Override public void log(String info) {
                System.out.println("Logging: " + info);
            }
        };

        public abstract void start();
        public abstract void stop();
        public abstract void log(String info);
    }

    private State _state;
    public Logger() {
        setState(State.STOPPED);
    }
    public void setState(State state) {
        _state = state;
    }
    public void start() {
        _state.start();
        setState(State.LOGGING);
    }
    public void stop() {
        _state.stop();
        setState(State.STOPPED);
    }
    public void log(String info) {
        _state.log(info);
    }
}

switch 문이 없어지고 _state.start(), _state.stop(), .state.log()처럼 _state에 작업을 위임하는 형태가 되었습니다. 

 

왜 이렇게 되는 걸까요? 그건 다형성의 전형적인 예이기 때문입니다. _state필드는 State 타입으로 상태에 따라 STATE_STOPPED 인스턴스 또는 STATE_LOGGING 인스턴스가 할당됩니다. 즉, _state.start()라는 표현식은 상태에 따라 STATE_STOPPED의 start 메서드 또는 STATE_LOGGING의 start 메서드 호출이 됩니다. 분기하는 코드가 switch 문처럼 눈에 보이는 건 아니지만 _state 값에 따라 호출되는 메서드가 다르다는 점을 이해하기 바랍니다. 

 

-229 ~ 231p 

 

 

상태 패턴과 전략 패턴의 차이 

 

리팩토링 이름에 등장하는 상태 패턴과 전략 패턴은 둘다 GoF 디자인 패턴입니다.

 

상태 패턴은 예제 프로그램에서 소개한 것처럼 프로그램 상태를 객체로 표현하고 상태에 의존하는 코드를 하위 클래스 메서드에 작성하는 것입니다. 이걸로 상태에 따른 switch 문이 없어져서 상태 전이도 깔끔하게 작성할 수 있습니다. 

 

전략 패턴은 하나로 정리된 처리를 하는 알고리즘을 조용히 전환할 때 사용하는 패턴입니다. 알고리즘 입출력을 인터페이스(API)로 규정해 두고 그 인터페이스(API)를 만족하는 구체적인 클래스를 선업합니다. 이렇게 하면 요구에 따른 알고리즘을 선택할 수 있어서 한 알고리즘을 다른 알고리즘으로 바궈서 재계산하는 일이 편해집니다. 

 

-234p 

 

 

 


에러 코드를 예외로 치환  

 

return null 

=> 

throw new InvalidCommandException(name); 

 

- 243p

 

 

 

인스턴스를 하나만 가지므로 다음처럼 익명 클래스를 쓰면 좀 더 단순하게 작성할 수 있습니다. 

 

public abstract class Command {

    private static class Forward extends Command {
        public Forward() {
            super("forward");
        }
        @Override public void execute(Robot robot) {
            robot.forward();
        }
    }

    private static class Backward extends Command {
        public Backward() {
            super("backward");
        }
        @Override public void execute(Robot robot) {
            robot.backward();
        }
    }

    private static class Right extends Command {
        public Right() {
            super("right");
        }
        @Override public void execute(Robot robot) {
            robot.right();
        }
    }

    private static class Left extends Command {
        public Left() {
            super("left");
        }
        @Override public void execute(Robot robot) {
            robot.left();
        }
}

 

이는 익명클래스로 싱글톤 패턴을 구현한 것입니다. 

- 259 ~ 260p 

 


 

생성자를 팩토리 메서드로 치환 

 

new 연산자를 사용해 생성자를 직접 호출하는 대신에 인스턴스를 생성하는 팩토리 메서드를 제공합니다. 

- 271p 

 

public static void main(String[] args) {
    Shape line = Shape.create(Shape.TYPECODE_LINE, 0, 0, 100, 200);
    Shape rectangle = Shape.create(Shape.TYPECODE_RECTANGLE, 10, 20, 30, 40);
    Shape oval = Shape.create(Shape.TYPECODE_OVAL, 100, 200, 300, 400);

    Shape[] shape = {
        line,
        rectangle,
        oval,
    };

    for (Shape s : shape) {
        s.draw();
    }
}

 

public static Shape create(int typecode, int startx, int starty, int endx, int endy) {
    return new Shape(typecode, startx, starty, endx, endy);
}

private Shape(int typecode, int startx, int starty, int endx, int endy) {
    _typecode = typecode;
    _startx = startx;
    _endx = endx;
    _starty = starty;
    _endy = endy;
}

 

 

- 285 ~ 287p

 

 

<패턴을 활용한 리팩터링>에서는 생성 메서드라는 용어를 새로 제안했습니다. 

  - 생성 메서드 = 인스턴스를 생성하는 메서드를 총칭

  - 팩토리 메서드 = GoF의 디자인 패턴과 의미가 같은 인스턴스 생성 메서드 

 

- 287p 

 

private final Printer _printer;
   
public Client(boolean graphical) {
    if (graphical) {
        _printer = new GraphPrinter();
    } else {
        _printer = new DigitPrinter();
    }
}

=> Client가 Printer, GraphPrinter, DogitPrinter에 모두 의존하고 있다는 문제 

 

=> 해결 

 

private final Printer _printer;
public Client(boolean graphical) {
    _printer = Printer.create(graphical);
}
public void execute() {
    int[] table = { 3, 1, 4, 1, 5, 9 };
    for (int n : table) {
        _printer.println(n);
    }
}

 

Printer에만 의존하도록 함 

 

private final Printer _printer;
public Client(Printer printer) {
    _printer = printer;
}
public void execute() {
    int[] table = { 3, 1, 4, 1, 5, 9 };
    for (int n : table) {
        _printer.println(n);
    }
}

= 의존성 주입 

 

Client 클래스의 생성자 매개변수를 변경해서 Printer 하위 클래스의 인스턴스를 직접 넘기도록 수정합니다. 그러면 Client 클래스는 DigitPrinter 클래스와 GraphPrinter 클래스에 의존하지 않습니다.

 

 

    public static void main(String[] args) {
        new Client(new DigitPrinter()).execute();
        new Client(new GraphPrinter()).execute();
    }
}

 

Client 인스턴스를 만들 때 Printer의 하위 클래스의 인스턴스를 넘긴다는 건 Client 인스턴스를 생성할 때 의존 관계를 만들어 낸다는 것이 됩니다. 이런 기법을 일반적으로 의존성주입이라고 부릅니다. DI라고 줄여 부르기도 합니다. 의존성 주입을 사용하면 클래스 의존 관계를 줄일 수 있으므로 클래스를 부품으로 다루기 쉬워집니다. 

 

의존성 주입으로 Client 클래스는 DigitPrinter 클래스나 GraphPrinter 클래스에 의존하지 않게 됩니다. 대신에 이번에는 Main 클래스가 양쪽 클래스에 존재합니다. 이렇듯 클래스 사이의 의존 관계 취급은 번거롭습니다. 

 

- 289 ~ 292p 

 


 

관측 데이터 복제 

 

이 리팩토링은 혼재하는 모델과 뷰를 분리합니다. 단지 분리할 뿐이라면 서로 동기화되지 않으므로 관찰자 패턴이나 이벤트 리스너를 사용해서 모델 내용이 변하면 그 사실을 뷰에 알리고 모델과 뷰를 동기화합니다. 이것이 관측 데이터 복제입니다. 

 

- 294p 

 

 

 

뷰에서 모델 참조 

 

private int _value = 0;

=> private Value _value = new Value(0); 

 

모델을 메서드로 조작 

 

_value.setValue(value); 

 

통지 내용을 나타내는 이벤트 선언 

 

public class ValueChangeEvent {
	private final Value _source;
    public ValueChangeEvent(Value source) {
    	_source = source;
    }

 

 

통지 관련 인터페이스 선언 

 

통지 메서드(valueChanged)를 포함한 인터페이스 (ValueListener)를 선언합니다. valueChanged 메서드에는 앞에서 선언한 ValueChangeEvent를 넘깁니다. 

 

public interface ValueListener {
	public void valueChanged(ValueChangeEvent e);
}

 

 

통지를 받는 메서드를 뷰에 선언 

 

public class IntegerDisplay extends Frame implements ActionListener, ValueListener {

   ... 

   public void valueChanged(ValueChangeEvent e) {

   }

}

 

 

- 303 ~ 304p 

 

 

모델 : 뷰를 모델에 등록 가능하게 만듬 

 

public class Value {
	...
    private final List<ValueListener> _listeners = new Array... 
    
    public void addValueListener(ValueListener listener) {

 

 

뷰 : 뷰를 모델에 등록 

 

IntegerDisplay 생성자에서 앞의 addValueListner를 호출합니다. 

 

public IntegerDisplay() {
	...
    _value.addValueListener(this); 
    ...

 

모델 : 모델을 변경하면 뷰에 통지하는 코드 작성 

 

notiyfyToListners(); 

 

private void notifyToListeners() {
	for (ValueListener listener : _listeners) {
    	listner.valueChanged(new ValueChangeEvent(this));

 

뷰 : 통지를 받는 메서드 안으로 표시 갱신 처리를 이동 

 

if (e.getSource() == _value) {
	_ocalLable.setText(Integer.tostaring...)

- 304 ~ 305p 

 


 

상속을 위임으로 치환 

 

 

class Another {
	void method() ...
    
    
class Something extends Another {
	...

=> 

class Something {
	Another _delegate = new Another();
    ...
    void method() {
    	_delegate.method();
    }
}

 

- 323p 

 

public class Dice {
    private final Random _random;
    public Dice() {
        _random = new Random(314159L);
    }
    public Dice(long seed) {
        _random = new Random(seed);
    }
    public int nextInt() {
        return _random.nextInt(6) + 1;
    }
    public void setSeed(long seed) {
        _random.setSeed(seed);
    }
}

 

 

 

 

기존에 상속받던 것을 생성자 호출로 전환 

 

- 332p 

 

 

상속은 최후의 무기 

- 335p 

 

 

IS - A 관계가 상속과 관련 있듯이 HAS - A 관계는 위임과 관계 있습니다. 그렇다면 ㅇㅇ 클래스의 인스턴스 필드가 ㅁㅁ 클래스의 인스턴스를 저장하고 있다면 ㅇㅇ은 ㅁㅁ를 가지고 있다 (ㅇㅇ has a ㅁㅁ)라는 HAS - A 관곅 ㅏ성립합니다. 

- 337p

 


대리자 은폐 

 

server.getDelegate().handle();

클라이언트 클래스가 대리 클래스를 이용하고 있음 (대리 클래스에 의존하는 상태)

 

server.handle();

클라리언트 클래스가 대리 클래스를 이용하지 않게 됨 (대리 클래스에 의존하지 않는 상태) 

 

- 344p 

 

 

반응형