본문 바로가기

국비 교육

2020.9.11일자 수업 : 익명 클래스, 커맨드 디자인 패턴

 익명 클래스 

git/eomcs-java-basic/src/main/java com.eomcs.oop.ex11.e

익명 클래스의 생성 방법

package com.eomcs.oop.ex11.e;

public class Exam0120 {
  
  interface A {
    void print();
  }
  
  public static void main(String[] args) {
    
    A x = new A() {
      @Override
      public void print() {
        System.out.println("Hello");
      }
    };
    
    x.print();
    
  }
}

 

인터페이스는 규칙이기 때문에 스태틱 멤버가 아니어도 다른 스태틱 메서드에서 사용 가능하다.

 

  • 클래스에 이름이 없으면 생성자를 만들 수 없으므로 호출할 때 익명 클래스의 생성자가 없다. 그래서 수퍼클래스의 생성자를 호출해야하는데 수퍼 클래스에서 정의된 생성자들들만 호출할 수 있으며 클래스의 선언과 동시에 딱 한번만 생성할 수 있다.
  • 인터페이스나 슈퍼 클래스를 오버라이딩할 것이 있으면 중괄호 안에 오버라이딩해서 사용할 수 있다.
  • 익명 클래스는 이름이 없기 때문에 익명 클래스를 특정하여 레퍼런스를 선언할 수 없다. 그래서 레퍼런스는 익명 클래스가 구현하는 인터페이스나 익명 클래스가 상속받는 수퍼 클래스로 선언해야한다.
  • 익명 클래스는 인터페이스를 구현하면서 동시에 어떤 클래스를 상속받을 수 없다. 여러개의 인터페이스를 구현할 수도 없다.

익명 클래스가 놓이는 장소

 

1. 스태틱 필드

// anonymous class - 익명 클래스가 놓이는 장소: 스태틱 필드
package com.eomcs.oop.ex11.e;

public class Exam0410 {
  // 인터페이스의 경우 static으로 선언하지 않아도 스태틱 멤버에서 사용할 수 있다.
  interface A {
    void print();
  }

  // 스태틱 필드의 값을 준비할 때 익명 클래스를 사용할 수 있다.
  static A obj = new A() {
    @Override
    public void print() {
      System.out.println("Hello!");
    }
  };
}

2. 인스턴스 필드

// anonymous class - 익명 클래스가 놓이는 장소: 인스턴스 필드
package com.eomcs.oop.ex11.e;

public class Exam0420 {
  // 인터페이스의 경우 static으로 선언하지 않아도 스태틱 멤버에서 사용할 수 있다.
  interface A {
    void print();
  }

  // 인스턴스 필드의 값을 준비할 때 익명 클래스를 사용할 수 있다.
  A obj = new A() {
    @Override
    public void print() {
      System.out.println("Hello!");
    }
  };
}

3. 로컬 변수

// anonymous class - 익명 클래스가 놓이는 장소: 로컬 변수
package com.eomcs.oop.ex11.e;

public class Exam0430 {
  // 인터페이스의 경우 static으로 선언하지 않아도 스태틱 멤버에서 사용할 수 있다.
  interface A {
    void print();
  }

  void m1() {
    // 로컬 변수의 값을 준비할 때 익명 클래스를 사용할 수 있다.
    A obj = new A() {
      @Override
      public void print() {
        System.out.println("Hello!");
      }
    };
    obj.print();
  }
  
  public static void main(String[] args) {
    Exam0430 r = new Exam0430();
    r.m1();
  }
}

4. 파라미터

// anonymous class - 익명 클래스가 놓이는 장소: 파라미터
package com.eomcs.oop.ex11.e;

public class Exam0440 {
  // 인터페이스의 경우 static으로 선언하지 않아도 스태틱 멤버에서 사용할 수 있다.
  interface A {
    void print();
  }

  void m1(A obj) {
    obj.print();
  }
  
  public static void main(String[] args) {
    Exam0440 r = new Exam0440();
    r.m1(new A() {
      @Override
      public void print() {
        System.out.println("안녕!");
      }
    });
  }
}

중첩 클래스를 활용한 카테고리 상수 활용법

기존에는 카테고리를 표현하는 상수를 지정하면 카테고리라는 클래스 하나에 다음과 같이 다양한 종류의 상수 필드를 두었다. 그러나 이렇게 하면 위의 계층의 이름을 바꾸게 될 경우, 그에 해당하는 모든 상수의 이름을 바꿔야한다. 예를 들어 BOOK을 BOOK2로 바꾼다면, BOOK_xxxx로 되어있는 모든 상수의 이름을 BOOK2_xxxx로 바꿔야한다.

package com.eomcs.oop.ex11.g.step5;

public class Category {
  public static final int COMPUTER_CPU = 1;
  public static final int COMPUTER_VGA = 2;
  public static final int COMPUTER_RAM = 3;
  public static final int COMPUTER_MOUSE = 4;
  public static final int COMPUTER_KEYBOARD = 5;

  public static final int APPLIANCE_TV = 10;
  public static final int APPLIANCE_AUDIO = 11;
  public static final int APPLIANCE_DVD = 12;
  public static final int APPLIANCE_VACUUMCLEANER = 13;

  public static final int BOOK_POET = 100;
  public static final int BOOK_NOVEL = 101;
  public static final int BOOK_ESSAY = 102;
  public static final int BOOK_IT = 103;
  public static final int BOOK_LANG = 104;
}

이러한 불편을 해결하기 위해 상위 계층을 Category안의 스태틱 중첩 클래스로 지정할 수 있다. 이렇게 하면 클래스가 그룹별로 클래스가 여러개 생기는 것을 방지하고, 계층적으로 작성도 가능하여 이해하기 쉽다. 중첩 클래스를 Category클래스의 필드처럼 보이게 하기 위해, 클래스의 이름을 소문자로 시작하게 했다.

// static nested class 문법을 이용하여 상수를 효과적으로 관리하기
package com.eomcs.oop.ex11.g.step7;

public class Category {
  public static class computer {
    public static final int CPU = 1;
    public static final int VGA = 2;
    public static final int RAM = 3;
    public static final int MOUSE = 4;
    public static final int KEYBOARD = 5;
  }

  public static class appliance {
    public static final int TV = 10;
    public static final int AUDIO = 11;
    public static final int DVD = 12;
    public static final int VACUUMCLEANER = 13;
  }

  public static class book {
    public static final int POET = 100;
    public static final int NOVEL = 101;
    public static final int ESSAY = 102;
    public static final int IT = 103;
    public static final int LANG = 104;
  }
}

 


 실습 - 익명 클래스 

git/eomcs-java-project-2020/mini-pms-26-d

AbstractList, Stack, Queue 클래스에 있는 iterator()의 메서드안에서 Iterator 구현체를 딱 한번만 생성하고 있다. 이렇다면 안에 들어있는 Iterator 구현체 로컬 클래스를 익명 클래스로 변경할 수 있다. 이를 통해 더 메서드를 간결하게 만들 수가 있다.

 

훈련 목표

iterator() 메서드에 있는 Iterator 구현체의 클래스를 익명 클래스로 변경한다. 

 

1단계 : AbstractList에 있는 iterator() 메서드에서 Iterator 구현체 클래스를 익명 클래스로 변경하고 이를 선언생성, 반환을 동시에 한다.

  @Override
  public Iterator<E> iterator() {
    // local class에는 로컬 변수처럼 접근 제어 키워드(private, protected, public)를 붙일 수 없다. 
    return new Iterator<E>() {
      int cursor;

      @Override
      public boolean hasNext() {
        // 로컬 클래스에서도 마찬가지로 바깥 클래스의 인스턴스 주소를 사용하고 싶다면 
        // 다음과 같이 바깥 클래스 이름으로 사용하라.
        return cursor < AbstractList.this.size();
      }

      @Override
      public E next() {
        // 물론 바깥 클래스의 인스턴스를 가리키는 AbstractList.this 를 생략할 수 있다.
        if (cursor ==  /*AbstractList.this.*/size())
          throw new NoSuchElementException();

        return get(cursor++);
      }
    };
  }

2단계 : Stack과 Queue에 있는 iterator() 메서드에서 Iterator 구현체 클래스를 익명 클래스로 변경하고 이를 바로 리턴하게 한다. Stack과 Queue의 경우에는, Iterator 구현체 안에 정의된 생성자를 인스턴스 블록으로 만들어주면 안에서 내부적으로 만들어진 생성자 안에 넣어줄것이다.

 @Override
  public Iterator<E> iterator() {
    return new Iterator<E>() {
      Stack<E> stack;

      {
        try {
          this.stack = Stack.this.clone();
        } catch (Exception e) {
          throw new RuntimeException("큐를 복제하는 중에 오류 발생!");
        }
      }

      @Override
      public boolean hasNext() {
        return !stack.empty();
      }

      @Override
      public E next() {
        if (stack.empty())
          throw new NoSuchElementException();
        return stack.pop();
      }
    };
  }
  @Override
  public Iterator<E> iterator() {

    return new Iterator<E>() {
      Queue<E> queue;

      {
        try {
          this.queue = Queue.this.clone();
        } catch (Exception e) {
          throw new RuntimeException("큐를 복제하는 중에 오류 발생!");
        }
      }

      @Override
      public boolean hasNext() {
        return queue.size() > 0;
      }

      @Override
      public E next() {
        if (queue.size() == 0)
          throw new NoSuchElementException();
        return queue.poll();
      }
    };
  }

 


 실습 - 자바 컬렉션 API 사용 

 

git/eomcs-java-project-2020/mini-pms-27

여태 자바 컬렉션을 그대로 사용하지 않고, 그것을 따라해서 직접 구현한 객체를 사용했다. 그러나 이제는 직접 만든 컬렉션 대신 기존의 자바 API를 사용하려고 한다. ArrayList, LinkedLIst, Stack, Queue 클래스들과, AbstractList 추상클래스, 그리고 LIst와 Iterator 인터페이스를 모두 자바 API로 변경한다.

 

훈련 목표

직접 만든 컬렉션 대신 자바 컬렉션 API를 사용한다.

 

1단계 : util 패키지에 있는 모든 컬렉션 클래스들과 인터페이스를 삭제한다. 

 

2단계 : command + shift + O 단축키를 이용해서 임포트가 안된 컬렉션들을 모두 임포트해준다. 다만 App에서 사용하고 있는 Stack는 Iterator로 뽑을 때 역순으로 항목이 꺼내지지 않기 때문에 Stack 인터페이스를 상속받은 다른 클래스 ArrayDequeue를 사용한다. 또한 Queue도 인터페이스이므로 인스턴스를 생성할 수 없기 때문에 이를 상속받은 LinkedList를 사용한다.

    ArrayDeque<String> commandStack = new ArrayDeque<>();
    Queue<String> commandQueue = new LinkedList<>();

 

 


 실습 - 커맨드 디자인 패턴 

 

git/eomcs-java-project-2020/mini-pms-28-a

지금은 명령어를 추가할 때마다 App에 있는 명령어 처리 코드를 추가시켜야한다. 이제는 기존 코드들에 최대한 손을 대지 않고 새로운  명령어 기능을 추가하고 싶다면, 커맨드 디자인 패턴을 사용하면 좋다.

 

커맨드 디자인 패턴

GoF의 유명한 디자인 패턴 중 하나이다. 메서드의 객체화 설계 기법으로, 한 개의 명령어를 처리하는 메서드를 별개의 클래스로 분리하는 기법이다. 이렇게 하면 명령어가 추가될 때마다 새 클래스를 만들면 되기 때문에 기존 코드를 손대지 않아 유지보수에 좋다. 즉 기존 소스에 영향을 끼치지 않고 새 기능을 추가하는 방식이다. 명령처리를 별도의 객체로 분리하기 때문에 실행 내역을 관리하기 좋고, 각 명령이 수행했던 작업을 다루기가 편하다. 인터페이스를 이용하면 메서드 호출 규칙을 단일화 할 수 있기 때문에 코딩의 일관성을 높혀줄 수 있다. 단 기능 추가할 때마다 해당 기능을 처리하는 새 클래스가 추가되기 때문에 클래스 개수는 늘어난다. 그러나 유지보수 관점에서는 소스 코드를 일관성 있게 유지보수 할 수 있는게 더 중요하다.

 

기존의 구조

"/board/add" 입력받으면 => boardHandler.add() 메서드 호출 

"/board/list" 입력받으면 => boardHandler.list() 메서드 호출

 

이처럼 명령어를 받으면 실행되는 메서드를 클래스로 정의한다. 즉, 메서드를 객체화하고. 메서드를 캡슐화한다. 

 

훈련 목표

  • 명령을 처리하는 역할을 수행하는 Command 인터페이스를 만든다.

  • 각 명령들을 처리하는 Command 구현체를 하나하나 만든다.

 

1단계 : 명령어를 처리할 Command 인터페이스와 그 안의 execute() 메서드를 정의한다. 

public interface Command {
  void execute();

}

2단계 : Member에 관한 각 명령을 처리할 Command의 구현체를 만들고 각 핸들러에서 해당 명령을 실행하는데 쓰였던 필드와 메서드들을 갖고온다. 

import java.util.List;
import com.eomcs.pms.domain.Member;
import com.eomcs.util.Prompt;

public class MemberAddCommand implements Command {

  List<Member> memberList;

  public MemberAddCommand(List<Member> list) {
    this.memberList = list;
  }
  
  @Override
  public void execute() {
    System.out.println("[회원 등록]");

    Member member = new Member();
    member.setNo(Prompt.inputInt("번호? "));
    member.setName(Prompt.inputString("이름? "));
    member.setEmail(Prompt.inputString("이메일? "));
    member.setPassword(Prompt.inputString("암호? "));
    member.setPhoto(Prompt.inputString("사진? "));
    member.setTel(Prompt.inputString("전화? "));
    member.setRegisteredDate(new java.sql.Date(System.currentTimeMillis()));

    memberList.add(member);
  }
}

3단계 : MemberHandler를 지우기전에 아직 옮기지 않은 findByName()을 MemberListCommand에 가져온다.

package com.eomcs.pms.handler;

import java.util.Iterator;
import java.util.List;
import com.eomcs.pms.domain.Member;

public class MemberListCommand implements Command {
  
  List<Member> memberList;

  public MemberListCommand(List<Member> list) {
    this.memberList = list;
  }

  @Override
  public void execute() {
    System.out.println("[회원 목록]");

    Iterator<Member> iterator = memberList.iterator();
    while (iterator.hasNext()) {
      Member member = iterator.next();
      System.out.printf("%d, %s, %s, %s, %s\n",
          member.getNo(),
          member.getName(),
          member.getEmail(),
          member.getTel(),
          member.getRegisteredDate());
    }
  }
  
  public Member findByName(String name) {
    for (int i = 0; i < memberList.size(); i++) {
      Member member = memberList.get(i);
      if (member.getName().equals(name)) {
        return member;
      }
    }
    return null;
  }
}

4단계 : MemberHandler를 삭제한다. BoardHandler, TaskHandler, ProjectHandler도 다음과 같이 Command 구현체에 옮긴후 삭제한다.

 

5단계 : App 클래스에서 Handler 클래스 대신 Command 구현체들을 생성하고 여기에 있는 execute() 메서드를 호출한다.

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import com.eomcs.pms.domain.Board;
import com.eomcs.pms.domain.Member;
import com.eomcs.pms.domain.Project;
import com.eomcs.pms.domain.Task;
import com.eomcs.pms.handler.BoardAddCommand;
import com.eomcs.pms.handler.BoardDeleteCommand;
import com.eomcs.pms.handler.BoardDetailCommand;
import com.eomcs.pms.handler.BoardListCommand;
import com.eomcs.pms.handler.BoardUpdateCommand;
import com.eomcs.pms.handler.HelloCommand;
import com.eomcs.pms.handler.MemberAddCommand;
import com.eomcs.pms.handler.MemberDeleteCommand;
import com.eomcs.pms.handler.MemberDetailCommand;
import com.eomcs.pms.handler.MemberListCommand;
import com.eomcs.pms.handler.MemberUpdateCommand;
import com.eomcs.pms.handler.ProjectAddCommand;
import com.eomcs.pms.handler.ProjectDeleteCommand;
import com.eomcs.pms.handler.ProjectDetailCommand;
import com.eomcs.pms.handler.ProjectListCommand;
import com.eomcs.pms.handler.ProjectUpdateCommand;
import com.eomcs.pms.handler.TaskAddCommand;
import com.eomcs.pms.handler.TaskDeleteCommand;
import com.eomcs.pms.handler.TaskDetailCommand;
import com.eomcs.pms.handler.TaskListCommand;
import com.eomcs.pms.handler.TaskUpdateCommand;
import com.eomcs.util.Prompt;

public class App {

  public static void main(String[] args) {
  
    List<Board> boardList = new ArrayList<>();
    BoardAddCommand boardAddCommand = new BoardAddCommand(boardList);
    BoardListCommand boardListCommand = new BoardListCommand(boardList);
    BoardDetailCommand boardDetailCommand = new BoardDetailCommand(boardList);
    BoardUpdateCommand boardUpdateCommand = new BoardUpdateCommand(boardList);
    BoardDeleteCommand boardDeleteCommand = new BoardDeleteCommand(boardList);
    

    List<Member> memberList = new LinkedList<>();
    MemberAddCommand memberAddCommand = new MemberAddCommand(memberList);
    MemberListCommand memberListCommand = new MemberListCommand(memberList);
    MemberDetailCommand memberDetailCommand = new MemberDetailCommand(memberList);
    MemberUpdateCommand memberUpdateCommand = new MemberUpdateCommand(memberList);
    MemberDeleteCommand memberDeleteCommand = new MemberDeleteCommand(memberList);

    List<Project> projectList = new LinkedList<>();
    ProjectAddCommand projectAddCommand = new ProjectAddCommand(projectList, memberListCommand);
    ProjectListCommand projectListCommand = new ProjectListCommand(projectList, memberListCommand);
    ProjectDetailCommand projectDetailCommand = new ProjectDetailCommand(projectList, memberListCommand);
    ProjectUpdateCommand projectUpdateCommand = new ProjectUpdateCommand(projectList, memberListCommand);
    ProjectDeleteCommand projectDeleteCommand = new ProjectDeleteCommand(projectList, memberListCommand);


    List<Task> taskList = new ArrayList<>();
    TaskAddCommand tasktAddCommand = new TaskAddCommand(taskList, memberListCommand);
    TaskListCommand taskListCommand = new TaskListCommand(taskList, memberListCommand);
    TaskDetailCommand taskDetailCommand = new TaskDetailCommand(taskList, memberListCommand);
    TaskUpdateCommand taskUpdateCommand = new TaskUpdateCommand(taskList, memberListCommand);
    TaskDeleteCommand taskDeleteCommand = new TaskDeleteCommand(taskList, memberListCommand);

    Deque<String> commandStack = new ArrayDeque<>();
    Queue<String> commandQueue = new LinkedList<>();

    loop:
      while (true) {
        String command = Prompt.inputString("명령> ");

        // 사용자가 입력한 명령을 보관한다.
        commandStack.push(command);
        commandQueue.offer(command);

        switch (command) {
          case "/member/add": memberAddCommand.execute(); break;
          case "/member/list": memberListCommand.execute(); break;
          case "/member/detail": memberDetailCommand.execute(); break;
          case "/member/update": memberUpdateCommand.execute(); break;
          case "/member/delete": memberDeleteCommand.execute(); break;
          case "/project/add": projectAddCommand.execute(); break;
          case "/project/list": projectListCommand.execute(); break;
          case "/project/detail": projectDetailCommand.execute(); break;
          case "/project/update": projectUpdateCommand.execute(); break;
          case "/project/delete": projectDeleteCommand.execute(); break;
          case "/task/add": tasktAddCommand.execute(); break;
          case "/task/list": taskListCommand.execute(); break;
          case "/task/detail": taskDetailCommand.execute(); break;
          case "/task/update": taskUpdateCommand.execute(); break;
          case "/task/delete": taskDeleteCommand.execute(); break;
          case "/board/add": boardAddCommand.execute(); break;
          case "/board/list": boardListCommand.execute(); break;
          case "/board/detail": boardDetailCommand.execute(); break;
          case "/board/update": boardUpdateCommand.execute(); break;
          case "/board/delete": boardDeleteCommand.execute(); break;
          case "history": printCommandHistory(commandStack.iterator()); break;
          // history2 명령을 처리한다.
          case "history2": printCommandHistory(commandQueue.iterator()); break;
          case "quit":
          case "exit":
            System.out.println("안녕!");
            break loop;
          default:
            System.out.println("실행할 수 없는 명령입니다.");
        }
        System.out.println(); // 이전 명령의 실행을 구분하기 위해 빈 줄 출력
      }
    Prompt.close();
  }

  static void printCommandHistory(Iterator<String> iterator) {
    try {
      int count = 0;
      while (iterator.hasNext()) {
        System.out.println(iterator.next());
        count++;

        // 5개 출력할 때 마다 계속 출력할지 묻는다.
        if ((count % 5) == 0 && Prompt.inputString(":").equalsIgnoreCase("q")) {
          break;
        }
      }
    } catch (Exception e) {
      System.out.println("history 명령 처리 중 오류 발생!");
    }
  }  
}

이렇게 클래스 구조를 바꾸면 앞으로 명령어를 추가할 때마다 그것을 처리하는 클래스만 추가해주면 된다. 

 

 


 실습 - Map 컬렉션 

git/eomcs-java-project-2020/mini-pms-28-b

이제 엄청 불어난 Command 구현체들을 한 곳에 관리할 수 있도록 Map 컬렉션 중 HashMap을 이용할 것이다. 

 

훈련 목표

  • HashMap 객체를 생성하고 명령어 문자열과 그에 해당하는 Command 구현체를 넣는다.

  • 이용자의 입력에 따라 HashMap에서 적절한 객체를 꺼내서 실행시킨다.

1단계 : HashMap 객체를 생성하고, 명령어 문자열을 key로, 그에 해당하는 Command 구현체를 value로 put 한다.

    List<Board> boardList = new ArrayList<>();
    commandMap.put("/board/add", new BoardAddCommand(boardList));
    commandMap.put("/board/list", new BoardListCommand(boardList));
    commandMap.put("/board/update", new BoardDetailCommand(boardList));
    commandMap.put("/board/detail", new BoardDetailCommand(boardList));
    commandMap.put("/board/delete", new BoardDeleteCommand(boardList));

    List<Member> memberList = new LinkedList<>();
    MemberListCommand memberListCommand = new MemberListCommand(memberList);
    commandMap.put("/member/add", new MemberAddCommand(memberList));
    commandMap.put("/member/list", memberListCommand);
    commandMap.put("/member/update", new MemberUpdateCommand(memberList));
    commandMap.put("/member/detail", new MemberDetailCommand(memberList));
    commandMap.put("/member/delete", new MemberDeleteCommand(memberList));
    
    List<Project> projectList = new LinkedList<>();
    commandMap.put("/project/add", new ProjectAddCommand(projectList, memberListCommand));
    commandMap.put("/project/list", new ProjectListCommand(projectList, memberListCommand));
    commandMap.put("/project/update", new ProjectUpdateCommand(projectList, memberListCommand));
    commandMap.put("/project/detail", new ProjectDetailCommand(projectList, memberListCommand));
    commandMap.put("/project/delete", new ProjectDeleteCommand(projectList, memberListCommand));
    
    List<Task> taskList = new ArrayList<>();
    commandMap.put("/task/add", new TaskAddCommand(taskList, memberListCommand));
    commandMap.put("/task/list", new TaskListCommand(taskList, memberListCommand));
    commandMap.put("/task/update", new TaskUpdateCommand(taskList, memberListCommand));
    commandMap.put("/task/detail", new TaskDetailCommand(taskList, memberListCommand));
    commandMap.put("/task/delete", new TaskDeleteCommand(taskList, memberListCommand));

2단계 : 입력값에 따라 실행하는 메서드를 달리했던 switch 문 명령어를 수정한다. commandMap에서 입력값에 해당하는 key를 이용해 value로 저장된 객체를 꺼낼 수 있도록 하면 많은 코드 줄을 하나로 줄일 수 있다. 꺼낸 Command 구현체의 execute를 실행한다.

        switch (inputStr) {
          case "history": printCommandHistory(commandStack.iterator()); break;
          case "history2": printCommandHistory(commandQueue.iterator()); break;
          case "quit":
          case "exit":
            System.out.println("안녕!");
            break loop;
          default:
            Command command = commandMap.get(inputStr);
            if (command != null) {
              command.execute();
            } else {
              System.out.println("실행할 수 없는 명령입니다.");
            }
        }