배경

  과제로 제출했던 코드 중 하나를 조금 더 객체지향적으로 변경하고 이를 설명하는 것을 주제로 학교 자바 수업의 기말 발표를 한 적이 있다. 과거 엉망인 코드를 짜면서 느꼈던 불편함과 그 당시 클린코드를 읽으면서 코드의 퀄리티와 객체지향에 대한 관심이 많아졌고, 이를 실습해보고 싶어 해당 주제를 선정했다. 그 때의 내용을 간단하게 기록하고자 한다.

 

문제

사용한 예제는 명품 자바 에센셜의 5장 실습문제 6번이다.

💡 [실습문제 6번] 간단한 그래픽 편집기를 만들어보자. 본문 5.6절의 메소드 오버라이딩과 5.7절의 추상 클래스의 설명 중에 Line, Rect, Circle 클래스 코드를 활용하여, 다음 실행 결과처럼 동작하는 프로그램을 작성하라.

삽입(1), 삭제(2), 모두보기(3), 종료(4) >> 1
도형 종류 LINE(1), RECT(2), CIRCLE(3) >> 1
삽입(1), 삭제(2), 모두보기(3), 종료(4) >> 1
도형 종류 LINE(1), RECT(2), CIRCLE(3) >> 3
삽입(1), 삭제(2), 모두보기(3), 종료(4) >> 3
Line
Circle
삽입(1), 삭제(2), 모두보기(3), 종료(4) >> 2
삭제할 도형 위치 >> 3
삭제할 수 없습니다.
삽입(1), 삭제(2), 모두보기(3), 종료(4) >> 4
프로그램을 종료합니다...

  사실 객체지향을 설명하는 단원의 문제여서, 문제의 요지도 다형성을 구현하는 것이었다. Draw()라는 추상함수를 갖는 추상 클래스 Shape를 Line, Rect, Circle로 구현하고, Shape 객체로 이를 업캐스팅해서 오버라딩된 함수를 호출하는 것을 구현하는 것이 문제의 정답이라고 할 수 있다.

 

풀이

  • shape.java

    abstract class Shape {
        public abstract void draw();
    }
    class Line extends Shape {
        @Override
        public void draw() {
            System.out.println("Line");
        }
    }
    class Rect extends Shape {
        @Override
        public void draw() {
            System.out.println("Rect");
        }
    }
    class Circle extends Shape {
        @Override
        public void draw() {
            System.out.println("Circle");
        }
    }
    

  • Main.java

    import java.util.*;
    
    public class Main {
    	public static void main(String[] args) {
    		var scanner = new Scanner(System.in);
    		var shapes = new Vector<Shape>();
    		int option = -1;
    
    		do {
    			System.out.println("삽입(1), 삭제(2), 모두보기(3), 종료(4) >> ");
    			option = scanner.nextInt();
    
    			switch(option) {
    			case 1:
    				System.out.println("도형 종류 Line(1), Rect(2), Circle(3) >> ");
    				int op2 = scanner.nextInt();
    
    				switch(op2) {
    				case 1:
    					shapes.add(new Line());
    					break;
    				case 2:
    					shapes.add(new Rect());
    					break;
    				case 3:
    					shapes.add(new Circle());
    					break;
    				}
    				break;
    			case 2:
    				System.out.println("삭제할 도형의 위치>> ");
    				int index = scanner.nextInt();
    				shapes.remove(index);
    				break;
    			case 3:
    				var itor = shapes.iterator();
    				while(itor.hasNext()) {
    					var obj = itor.next();
    					obj.draw();
    				}
    			}
    		} while(option != 4);
    	}
    }
    

예외 처리 같은 더 신경써줘야 할 것들이 있지만 일단은 생략했다.

 

더 생각해볼 문제

반복되는 동일 구조

다만, 위 코드는 불편함이 존재한다. 선택지를 구현하는 코드는 동일한 구조가 반복된다. 그러나, 이를 반복해서 작성하고 있다. 즉, DRY하지 않다! 만약 선택지가 늘어난다면 늘어내는대로 선택지를 중첩해서 추가 작성해야 한다. 아마 굉장히 꼴뵈기 힘든 코드가 만들어 질 것이다. 위의 코드도 벌써 도형을 삽입할 때, 2개의 swtich문이 반복해서 나타난다.

공통구조.png

절차적인 코드

main 함수에 모든 코드가 담겨 있다는 것이 문제가 될 수도 있다. 클린 코드에 따르면 함수와 클래스는 최대한 작고 하나의 책임만을 가져야 한다고 한다. 그러나 지금 main에서 모든 일을 담당하고 있다. 이를 분리할 필요가 있다.

 

객체지향으로

1. Vector와 Scanner 클래스에 대한 Wrapper

외부 코드 감싸기.png

클린 코드에서 외부 라이브러리를 사용하게 될 때, 외부 라이브러리의 변화가 미치는 정도를 줄이기 위해서 그 라이브러리의 클래스들을 한 번 감싸 경계를 구분해야 한다고 했다. 그래서, 앞서 사용한 Vector를 감싼 ShapeManagerScanner를 감싼 Prompt 클래스를 만든다.

  • ShapeManager

    • ShapeManager는 특히 일급 컬렉션이라고 하는 것 같은데, 이름이 멋있는 것 치고는 특별한 의미가 있다고 생각하지는 않는다. 다만, 뭔가 조금 더 기능 단위를 확실하게 구분하는 느낌이다. 그 외에는 어떤 데이터 객체에 특별한 이름을 붙인 것이 조금 더 특별하다면 특별하다고 할 수 있을 것 같다.
    import java.util.Iterator;
    import java.util.Vector;
    
    public class ShapeManager {
        Vector<Shape> store;
    
        public void insert(Shape s) {
            store.add(s);
        }
    
        public int delete(int index) {
            try {
                store.remove(index);
            } catch (ArrayIndexOutOfBoundsException e) {
                return -1;
            }
            return 0;
        }
        public void printAll() {
            Iterator<Shape> iter = store.iterator();
            while(iter.hasNext())
                iter.next().draw();
        }
    }
    
  • Prompt

    • Prompt의 경우 더 좋은 클래스 이름이 있을 것 같지만 영어 능력의 한계로 Prompt라고 했다.
    import java.util.InputMismatchException;
    import java.util.Scanner;
    
    public class Prompt {
        Scanner scanner;
        public Prompt() {
            scanner = new Scanner(System.in);
        }
        
        public int prompt() {
            int n;
            try {
                n = scanner.nextInt();
            } catch(InputMismatchException e) {
                n = -1;
            }
            return n;
        }
        
        public void close() {
            scanner.close();
        }
    }
    

이렇게 하면, 변경이 존재할 때, 대응하기가 쉬워진다. 해당 클래스만 변경해주면 되니까 말이다. Prompt의 경우 만약 시스템을 cui에서 gui로 변경하거나 유사한 변경사항이 있을 때, 인터페이스만 잘 설계했다면, Prompt만 수정하고 나머지 Prompt를 사용하는 코드는 수정이 거의 필요 없을 것이다. 이를 캡슐화와 정보 은닉의 장점이라고 할 수 있다.

2. 다형성을 이용해 구조 변경하기

한 선택지 분기마다 하나의 클래스를 작성한다. 각 클래스는 하나의 추상클래스를 상속 받는다. 이를 **State**이라고 이름 붙였고, 이를 구현하는 클래스의 이름 끝에도 State이라는 단어를 붙였다. 이것 역시 State보다 더 좋은 이름이 있을 거라고 생각하지만, 역시 역량이 부족했다.

각 클래스가 구현해야하는 것들은 다음과 같다.

  1. 출력 메시지
  2. 해당 분기가 입력을 요구하는지 여부
  3. 분기에서 처리해야할 메인 로직
  • State

    public abstract class State {
        protected boolean prompt;
        public State(boolean prompt) {
            this.prompt = prompt;
        }
        public boolean isNeedPrompt() {
            return prompt;
        }
        
        public abstract void printMsg();
        public abstract State process(int input, ShapeManager sm);
    }
    
  • MainState

    class MainState extends State {
        public MainState() {
            super(true);
        }
    
        @Override
        public void printMsg() {
            System.out.print("삽입(1), 삭제(2), 모두 보기(3), 종료(4)");  
        }
    
        @Override
        public State process(int input, ShapeManager sm) {
            switch(input) {
            case 1: return new InsertState();
            case 2: return new DeleteState();
            case 3:
                sm.printAll();
                break;
            case 4: return null;
            default :
                System.out.println("잘못된 선택지입니다. 다시 선택해주세요.");
            }
            return this;
        }
    }
    
  • InsertState

    public class InsertState extends State {
        public InsertState() {
            super(true);
        }
        @Override
        public void printMsg() {
            System.out.print("도형 종류 Line(1), Rect(2), Circle(3) >>");
        }
        @Override
        public State process(int input, ShapeManager sm) {
            switch(input) {
            case 1:
                sm.insert(new Line());
                break;
            case 2:
                sm.insert(new Rect());
                break;
            case 3:
                sm.insert(new Circle());
                break;
            default :
                System.out.println("잘못된 선택지입니다. 다시선택하세요.");
                return this;
            }
            return new MainState();
        }
    }
    
  • DeleteState

    public class DeleteState extends State {
        public DeleteState() {
            super(true);
        }
    
        @Override
        public void printMsg() {
            System.out.print("삭제할 도형의 위치 >>");
        }
    
        @Override
        public State process(int input, ShapeManager sm) {
            if(!sm.delete(input))
                System.out.println("삭제할 수 없습니다.");
            return new MainState();
        }
    }
    

이제 선택지가 더 필요한 경우에는 State 클래스를 하나 더 만들고, 다른 클래스와 연관시키기만 하면 된다. 다만, 개선사항이 있다면, 해당 구조는 클래스간 연관관계를 잘 살펴봐야 한다는 것이다. 그리고 본질적으로 스위치를 없애고 싶었지만 그러지 못했다.

3. main에서 조립

  • GraphicEditor - Main

    public class GraphicEditor {
        public static void main(String[] args) {
            Prompt prompt = new Prompt();
            ShapeManager shapes = new ShapeManager();
            State state = new MainState();
            
            while(state != null) {
                int inputValue = -1;
                state.printMsg();
                if(state.isNeedPrompt()) inputValue = prompt.prompt();
                var nextState = state.process(inputValue, shapes);
                state = nextState;
            }
            
            prompt.close();
        }
    }
    
  • main에서는 작성했던 클래스들을 조립했다.

  • 정확하게 메인 루프에는 선택 안내 메시지 출력(printMsg()) → 선택지 입력(prompt()) → 선택지에 따른 행동(process())이라는 한가지 구조만 눈에 보인다. 다형성을 이용해서 동일한 구조에서 다른 행동을 할 수 있게 되었다.

  • 다른 선택지를 추가해야할 때에는 클래스를 작성하면 된다.

4. 클래스 다이어그램

클래스다이어그램.png

코드들을 정리해서 다이어그램으로 나타냈다. 다이어그램은 파워포인트로 작성했는데, 표현의 한계가 있을 수도 있고, 다이어그램 작성을 처음 해본 탓에 오류가 있을 수도 있다.

더 개선할 수 있을까?

switch 줄이기

사실 처음 고민 했을 때에는 switch문 자체를 줄이고 싶었다. 지금 당장 생각나는 방법은 메소드 레퍼런스를 배열로 관리하는 것이라고 생각한다.

int input = ...; // 입력 받고, 입력값 검증하는 단계
method[input]();. // method는 State이 관리

C#에서는 delegate를 통해서 가능할 것 같다는 생각이 드는데, 자바에서는 그런게 있는가 모르겠다. 이게 함수형 프로그래밍 기법 중 일종이라고 알고 있는데, 아마 자바도 지원하지 않을까 생각한다.

복잡해진 구조

클래스를 추가로 더 만들고, 코드를 여러 클래스로 분리하다 보니까 초기 해답보다 복잡해지고, 코드가 조금 더 길어진 감은 있다. 객체지향이 갖는 어쩔 수 없는 점이라고 생각하지만, 더 좋은 구조가 있겠다는 생각이 든다. 경험이나 지식이 좀 부족하다.