본문 바로가기

국비 교육

2020.9.2일자 수업 : 미니 프로젝트 실습 - Stack, Queue

 실습 - LinkedList 적용 

git/eomcs-java-project/src/mini-pms-19

 

이번에는 ArrayList 대신 LinkedList를 사용해보려고 한다.

 

LinkedList의 장단점

  • 각 항목들의 연결 고리를 수정하기만 하면 되므로 항목 삽입, 삭제에 걸리는 시간이 ArrayList보다 작다.
  • 배열이 커질 때마다 가비지가 생기는 ArrayLIst에 비해 기존의 객체들을 유지하기 때문에 메모리 관리가 효율적이다.
  • 어떤 항목을 조회할때 첫 항목부터 원하는 항목까지 모두 조회하는 과정을 거치므로 항목 조회에 걸리는 시간이 ArrayList보다 크다.

훈련 목표

ArrayList를 쓰던 것을 LinkedList로 대체한다.

 

1단계 : LinkedList를 사용하기 전에 저번에 실습한 LinkedList를 마저 완성하려고 한다. 

  • 제네릭 적용

  • E[] toArray(E[] arr) E[] 배열을 받아 그 배열에 LinkedList 항목들을 복사하여 리턴하는 메서드 추가

package com.eomcs.algorithm.data_structure.linkedlist;

import java.lang.reflect.Array;

public class MyLinkedList<E> {
  
  private Node<E> first;
  
  private Node<E> last;
  
  private int size;
 
  static class Node<E> {
    E value;
    Node<E> next;
    
    public Node() {}
    
    public Node(E value) {
      this.value = value;
    }
  }
  
  public boolean add(E e) {
    Node<E> node = new Node<>();
    node.value = e;
    
    if (first == null) {
      first = node;
    } else {
      last.next = node;
    }
    last = node;
    size++;
    return true;
  }
  
  public void add(int index, E element) {
    if (index < 0 || index > size) {
      throw new IndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    
    Node<E> node = new Node<>(element);
    
    size++;
    
    if (index == 0) {
      node.next = first;
      first = node;
      return ;
    }
    Node<E> cursor = this.first;
    for (int i = 1; i <= index - 1; i++ ) {
      cursor = cursor.next;
    }
    node.next = cursor.next;
    cursor.next = node;
    if (node.next == null) {
      last = node;
    }
  }
  public E get(int index) {
    if (index < 0 || index >= this.size) {
      throw new IndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    Node<E> cursor = this.first;
    for (int i = 1; i <= index; i++ ) {
      cursor = cursor.next;
    }
    return cursor.value;
  }
  
  public E remove(int index) {
    if (index < 0 || index >= size) {
      throw new IndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    
    size--;
    
    if (index == 0) {
      Node<E> old = first;
      first = first.next;
      old.next = null;
      return old.value;
    }
    Node<E> cursor = this.first;
    for (int i = 1; i <= index - 1; i++ ) {
      cursor = cursor.next;
    }
    
    if (cursor.next == null) {
      last = cursor;
    }
   
    Node<E> old = cursor.next;
    cursor.next = old.next;
    old.next = null; // 가비지가 다른 인스턴스를 가리키지 않게한다.
    
    return old.value;
  }
  
  public E set(int index, E element) {
    if (index < 0 || index >= this.size) {
      throw new IndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    Node<E> cursor = this.first;
    for (int i = 1; i <= index; i++ ) {
      cursor = cursor.next;
    }
    E old = cursor.value;
    cursor.value = element;
    return old;
  }
  public Object[] toArray() {
    Object[] arr = new Object[this.size];
    
    int i = 0;
    Node<E> cursor = first;
    while (cursor != null) {
      arr[i++] = cursor.value;
      cursor = cursor.next;
    }
    return arr;
  }
  
  public int size() {
    return this.size;
  }
 
  @SuppressWarnings("unchecked")
  public E[] toArray(E[] arr) {
    if (arr.length < this.size()) {
      arr = (E[]) Array.newInstance(arr.getClass().getComponentType(), this.size());
    }
    Object[] originArr = this.toArray();
    for (int i = 0; i < this.size(); i++) {
      arr[i] = (E) originArr[i];
    }
    return arr;
  }
}

2단계 : LinkedList를 갖고 와서 ArrayList를 대체시키고, Handler클래스에서 ArrayList를 사용하는 코드를 수정한다. ArrayList와 LinkedList의 기능은 동일하기 때문에 이름만 바꿔도 오류가 없다.

package com.eomcs.pms.handler;

import java.sql.Date;
import com.eomcs.pms.domain.Board;
import com.eomcs.util.LinkedList;
import com.eomcs.util.Prompt;

public class BoardHandler {

  LinkedList<Board> boardList = new LinkedList<>();

  public void add() {
    System.out.println("[게시물 등록]");

    Board board = new Board();
    board.setNo(Prompt.inputInt("번호? "));
    board.setTitle(Prompt.inputString("제목? "));
    board.setContent(Prompt.inputString("내용? "));
    board.setWriter(Prompt.inputString("작성자? "));
    board.setRegisteredDate(new Date(System.currentTimeMillis()));
    board.setViewCount(0);

    boardList.add(board);

    System.out.println("게시글을 등록하였습니다.");
  }

  public void list() {
    System.out.println("[게시물 목록]");

    for (int i = 0; i < boardList.size(); i++) {
      Board board = boardList.get(i);
      System.out.printf("%d, %s, %s, %s, %d\n",
          board.getNo(),
          board.getTitle(),
          board.getWriter(),
          board.getRegisteredDate(),
          board.getViewCount());
    }
  }

  public void detail() {
    System.out.println("[게시물 상세보기]");
    int no = Prompt.inputInt("번호? ");
    Board board = findByNo(no);

    if (board == null) {
      System.out.println("해당 번호의 게시글이 없습니다.");
      return;
    }

    board.setViewCount(board.getViewCount() + 1);

    System.out.printf("제목: %s\n", board.getTitle());
    System.out.printf("내용: %s\n", board.getContent());
    System.out.printf("작성자: %s\n", board.getWriter());
    System.out.printf("등록일: %s\n", board.getRegisteredDate());
    System.out.printf("조회수: %d\n", board.getViewCount());
  }

  public void update() {
    System.out.println("[게시물 변경]");
    int no = Prompt.inputInt("번호? ");
    Board board = findByNo(no);

    if (board == null) {
      System.out.println("해당 번호의 게시글이 없습니다.");
      return;
    }

    String title = Prompt.inputString(
        String.format("제목(%s)? ", board.getTitle()));
    String content = Prompt.inputString(
        String.format("내용(%s)? ", board.getContent()));
    String writer = Prompt.inputString(
        String.format("작성자(%s)? ", board.getWriter()));

    String response = Prompt.inputString("정말 변경하시겠습니까?(y/N) ");
    if (!response.equalsIgnoreCase("y")) {
      System.out.println("게시글 변경을 취소하였습니다.");
      return;
    }

    board.setTitle(title);
    board.setContent(content);
    board.setWriter(writer);
    System.out.println("게시글을 변경하였습니다.");
  }

  public void delete() {
    System.out.println("[게시물 삭제]");
    int no = Prompt.inputInt("번호? ");
    int index = indexOf(no);

    if (index == -1) {
      System.out.println("해당 번호의 게시글이 없습니다.");
      return;
    }

    String response = Prompt.inputString("정말 삭제하시겠습니까?(y/N) ");
    if (!response.equalsIgnoreCase("y")) {
      System.out.println("게시글 삭제를 취소하였습니다.");
      return;
    }

    boardList.remove(index);
    System.out.println("게시글을 삭제하였습니다.");
  }

  private Board findByNo(int no) {
    for (int i = 0; i < boardList.size(); i++) {
      Board board = boardList.get(i);
      if (board.getNo() == no) {
        return board;
      }
    }
    return null;
  }

  private int indexOf(int no) {
    for (int i = 0; i < boardList.size(); i++) {
      Board board = boardList.get(i);
      if (board.getNo() == no) {
        return i;
      }
    }
    return -1;
  }
}

 실습 2 - Stack 사용 

git/eomcs-java-project/src/mini-pms-20

Stack은 가장 마지막에 추가된 항목이 가장 처음에 나오는 방식의 컬렉션 클래스이다. 

이것을 후입 선출 (Last In First Out)이라고 하며 웹 브라우저에서 뒤로 가기와 원리가 같다.

 

따라서 프로젝트에서 이용자의 행위에 대한 히스토리를 저장하는 데 스택을 사용해볼 것이다.

 

훈련 목표

스택을 사용해 이용자가 입력한 커맨드 라인을 저장하고, 이를 가장 최근에 입력한 것부터 거꾸로 조회할 수 있는 기능을 추가한다.

 

1단계 : 먼저 스택이 아닌 ArrayList를 갖고 커맨드 라인 저장하며 역순으로 조회하는 메서드를 추가한다. 역순으로 다섯번 출력할 때마다 한번씩 중지하는 기회를 준다.

package com.eomcs.pms;

import com.eomcs.pms.handler.BoardHandler;
import com.eomcs.pms.handler.MemberHandler;
import com.eomcs.pms.handler.ProjectHandler;
import com.eomcs.pms.handler.TaskHandler;
import com.eomcs.util.ArrayList;
import com.eomcs.util.Prompt;

public class App {

  public static void main(String[] args) {

    BoardHandler boardHandler = new BoardHandler();
    MemberHandler memberHandler = new MemberHandler();
    ProjectHandler projectHandler = new ProjectHandler(memberHandler);
    TaskHandler taskHandler = new TaskHandler(memberHandler);
    
    ArrayList<String> commandList = new ArrayList<>();
    
    loop:
      while (true) {
        String command = Prompt.inputString("명령> ");
        
        commandList.add(command);
        switch (command) {
          case "/member/add": memberHandler.add(); break;
          case "/member/list": memberHandler.list(); break;
          case "/member/detail": memberHandler.detail(); break;
          case "/member/update": memberHandler.update(); break;
          case "/member/delete": memberHandler.delete(); break;
          case "/project/add": projectHandler.add(); break;
          case "/project/list": projectHandler.list(); break;
          case "/project/detail": projectHandler.detail(); break;
          case "/project/update": projectHandler.update(); break;
          case "/project/delete": projectHandler.delete(); break;
          case "/task/add": taskHandler.add(); break;
          case "/task/list": taskHandler.list(); break;
          case "/task/detail": taskHandler.detail(); break;
          case "/task/update": taskHandler.update(); break;
          case "/task/delete": taskHandler.delete(); break;
          case "/board/add": boardHandler.add(); break;
          case "/board/list": boardHandler.list(); break;
          case "/board/detail": boardHandler.detail(); break;
          case "/board/update": boardHandler.update(); break;
          case "/board/delete": boardHandler.delete(); break;
          case "history": printCommandHistory(commandList); break;
          case "quit":
          case "exit":
            System.out.println("안녕!");
            break loop;
          default:
            System.out.println("실행할 수 없는 명령입니다.");
        }
        System.out.println(); // 이전 명령의 실행을 구분하기 위해 빈 줄 출력
      }

    Prompt.close();
  }

  private static void printCommandHistory(ArrayList<String> commandList) {
    for (int i = commandList.size() - 1, count = 1; i >= 0; i--, count++) {
      System.out.println(commandList.get(i));
      
      if ((count % 5) == 0) {
        String response = Prompt.inputString(":");
        if (response.equalsIgnoreCase("q")) {
          break;
        }
      }
    }
  }
}

2단계 : 이번에는 MyStack에서 스택이 비어있는 지 확인해주는 empty() 메서드를 추가한다. 그리고 이 Stack을 갖고와서 App에서 LinkedList 대신 Stack을 사용하도록 한다. * Stack을 사용하면 우리가 직접 역순으로 출력할 필요가 없다.

  public boolean empty() {
    return this.size() == 0;
  }
  Stack commandList = new Stack();

  String command = Prompt.inputString("명령> ");
  commandList.push(command);

  private static void printCommandHistory(Stack commandList) {
    for (int count = 1; !commandList.empty(); count++) {
      System.out.println(commandList.pop());
      
      if ((count % 5) == 0) {
        String response = Prompt.inputString(":");
        if (response.equalsIgnoreCase("q")) {
          break;
        }
      }
    }
  }

문제점 : 이렇게 조회하면 한번 조회할때마다 항목이 빠져나가기 때문에 두 번 조회가 불가능하다.

 

3단계 : 이 문제를 해결하기 위해 스택을 복사하여 그것을 출력해야한다. 그러기 위해서는 Stack에 clone() 메서드가 필요하다. clone()를 기존의 MyStack에서 오버라이딩한다. 클래스 선언부에 implements Cloneable을 추가하고 clone() throws CloneNotSupportedException을 추가한다.

package com.eomcs.algorithm.data_structure.stack;

import java.util.EmptyStackException;
import com.eomcs.algorithm.data_structure.linkedlist.MyLinkedList;

public class MyStack extends MyLinkedList implements Cloneable {
  
  public Object push(Object item) { 
    add(item);
    return item;
  }
  
  public Object pop() {
    if (size() == 0) {
      throw new EmptyStackException();
    }
    return remove(this.size() - 1);
  }
  
  public Object peek() {
    if (size() == 0) {
      throw new EmptyStackException();
    }
    return get(this.size() - 1);
  }
  
  public boolean empty() {
    return this.size() == 0;
  }
  
  @Override
  public MyStack clone() throws CloneNotSupportedException {
    return (MyStack) super.clone();
  }
}

4단계 : 그런데 이렇게 하면 Stack의 필드인 첫번째 노드와 마지막 노드의 주소값만 복사되므로 결국 원본과 같은 노드들을 공유하게 된다. 이것을 shallow copy라고 한다. 이렇게 되면, 원본 Stack에 항목을 지우면 복사된 Stack의 항목도 함께 지워지지만, size는 줄지 않아 NullPointerException이 뜬다. 따라서 deep copy를 하기 위해서는 원본과는 별도의 노드들을 만들어줘야한다. 

  @Override
  public MyStack clone() throws CloneNotSupportedException {
      MyStack newStack = new MyStack();
      
      Object[] values = this.toArray();
      for (Object value : values) {
        newStack.push(value);
    }
    return newStack;
  }

5단계 : 이렇게 바꿔줬다면 App으로 가서 Stack을 복사한 복사본을 출력하도록 메서드를 수정한다. 복사 과정에서 생길 수 있는 오류를 처리하기 위해 해당 코드들을 try/catch안에 넣는다. 이러면 원본의 항목은 영향을 받지 않기 때문에 다음에 또 조회해도 항목이 사라지지 않는다.

  private static void printCommandHistory(Stack commandList) {
    try {
      Stack commandStack = commandList.clone(); 
      for (int count = 1; !commandStack.empty(); count++) {
        System.out.println(commandStack.pop());
        
        if ((count % 5) == 0) {
          String response = Prompt.inputString(":");
          if (response.equalsIgnoreCase("q")) {
            break;
          }
        }
      }
    } catch (Exception e) {
      System.out.println("history 명령 처리 중 오류 발생");
    }
  }

6단계 : 마지막으로 스택에 제네릭을 적용하고 App에서 생성하는 객체의 타입 파라미터를 String으로 지정해준다. printCommandHistory()에서 사용되는 메서드 System.out.println() 은 파라미터로 Object를 받기 때문에 어떤 리턴값을 받아 파라미터로 넘겨도 상관없다. 따라서 조회 목적으로만 사용되는 printCommandHistory()의 파라미터는 Stack<?>이어도 된다.

package com.eomcs.util;

import java.util.EmptyStackException;

public class Stack<E> extends LinkedList<E> implements Cloneable {
  
  public E push(E item) { 
    add(item);
    return item;
  }
  
  public E pop() {
    if (size() == 0) {
      throw new EmptyStackException();
    }
    return remove(this.size() - 1);
  }
  
  public E peek() {
    if (size() == 0) {
      throw new EmptyStackException();
    }
    return get(this.size() - 1);
  }
  
  public boolean empty() {
    return this.size() == 0;
  }
  
  @SuppressWarnings("unchecked")
  @Override
  public Stack<E> clone() throws CloneNotSupportedException {
      Stack<E> newStack = new Stack<>();
      
      Object[] values = this.toArray();
      for (Object value : values) {
        newStack.push((E) value);
    }
    return newStack;
  }
}
package com.eomcs.pms;

import com.eomcs.pms.handler.BoardHandler;
import com.eomcs.pms.handler.MemberHandler;
import com.eomcs.pms.handler.ProjectHandler;
import com.eomcs.pms.handler.TaskHandler;
import com.eomcs.util.Prompt;
import com.eomcs.util.Stack;

public class App {

  public static void main(String[] args) {

    BoardHandler boardHandler = new BoardHandler();
    MemberHandler memberHandler = new MemberHandler();
    ProjectHandler projectHandler = new ProjectHandler(memberHandler);
    TaskHandler taskHandler = new TaskHandler(memberHandler);
    
    Stack<String> commandList = new Stack<>();
    
    loop:
      while (true) {
        String command = Prompt.inputString("명령> ");
        
        commandList.push(command);
        switch (command) {
          case "/member/add": memberHandler.add(); break;
          case "/member/list": memberHandler.list(); break;
          case "/member/detail": memberHandler.detail(); break;
          case "/member/update": memberHandler.update(); break;
          case "/member/delete": memberHandler.delete(); break;
          case "/project/add": projectHandler.add(); break;
          case "/project/list": projectHandler.list(); break;
          case "/project/detail": projectHandler.detail(); break;
          case "/project/update": projectHandler.update(); break;
          case "/project/delete": projectHandler.delete(); break;
          case "/task/add": taskHandler.add(); break;
          case "/task/list": taskHandler.list(); break;
          case "/task/detail": taskHandler.detail(); break;
          case "/task/update": taskHandler.update(); break;
          case "/task/delete": taskHandler.delete(); break;
          case "/board/add": boardHandler.add(); break;
          case "/board/list": boardHandler.list(); break;
          case "/board/detail": boardHandler.detail(); break;
          case "/board/update": boardHandler.update(); break;
          case "/board/delete": boardHandler.delete(); break;
          case "history": printCommandHistory(commandList); break;
          case "quit":
          case "exit":
            System.out.println("안녕!");
            break loop;
          default:
            System.out.println("실행할 수 없는 명령입니다.");
        }
        System.out.println(); // 이전 명령의 실행을 구분하기 위해 빈 줄 출력
      }

    Prompt.close();
  }

  private static void printCommandHistory(Stack<?> commandList) {
    try {
      Stack<?> commandStack = commandList.clone(); 
      for (int count = 1; !commandStack.empty(); count++) {
        System.out.println(commandStack.pop());
        
        if ((count % 5) == 0) {
          String response = Prompt.inputString(":");
          if (response.equalsIgnoreCase("q")) {
            break;
          }
        }
      }
    } catch (Exception e) {
      System.out.println("history 명령 처리 중 오류 발생");
    }
  }
}

 

 

 실습 3 - Queue 사용 

git/eomcs-java-project/src/mini-pms-21

훈련목표

Stack과 똑같은 방식으로 commandList2를 저장하고 이를 순서대로 출력할 수 있도록 기능을 추가한다. 

  • empty() 메서드 추가

  • clone() 메서드를 추가해서 deep copy 구현

  • 제네릭 적용

1단계 : commandList2를 만들어 사용자가 입력한 것을 모두 저장한다. printCommandHistory2(Queue)도 구현한다.

  Queue commandList2 = new Queue();

  String command = Prompt.inputString("명령> ");
  commandList2.add(command);

  private static void printCommandHistory(Queue commandList2) {
    for (int count = 1; commandList2.size() > 0; count++) {
      System.out.println(commandList2.poll());
      
      if ((count % 5) == 0) {
        String response = Prompt.inputString(":");
        if (response.equalsIgnoreCase("q")) {
          break;
        }
      }
    }
  }

2단계 : clone() 메서드를 추가하여 Stack과 같은 방식으로 deep copy를 구현한다. 클래스 선언부에도 implements Cloneable를 붙인다. App 클래스에서 원본을 복사한 객체를 출력한다.

public class MyQueue extends MyLinkedList implements Cloneable {
  @Override
  public MyQueue clone() throws CloneNotSupportedException {
    MyQueue newQue =  new MyQueue();
    Object[] values = this.toArray();
    for (Object value : values) {
      newQue.offer(value);
    }
    return newQue;
  }
}
  Queue commandList2 = new Queue();

  String command = Prompt.inputString("명령> ");
  commandList2.offer(command);

  private static void printCommandHistory(Queue commandList2) {
    try {
      Queue commandQueue = commandList2.clone();
      for (int count = 1; commandQueue.size() > 0; count++) {
        System.out.println(commandQueue.poll());
        
        if ((count % 5) == 0) {
          String response = Prompt.inputString(":");
          if (response.equalsIgnoreCase("q")) {
            break;
          }
        }
      }
    } catch (Exception e) {
      System.out.println("history2 명령 처리 중 오류 발생");
    }
  }

3단계 : 제네릭을 적용하고 마지막으로 App 클래스를 수정한다.

package com.eomcs.util;

public class Queue<E> extends LinkedList<E> implements Cloneable {
  public boolean offer(E e) {
    return this.add(e);
  }
  
  public E poll() {
    if (size() == 0) {
      return null;
    }
    return remove(0);
  }
  public E peek() {
    if (size() == 0) {
      return null;
    }
    return get(0);
  }
  
  @SuppressWarnings("unchecked")
  @Override
  public Queue<E> clone() throws CloneNotSupportedException {
    Queue<E> newQue =  new Queue<>();
    Object[] values = this.toArray();
    for (Object value : values) {
      newQue.offer((E) value);
    }
    return newQue;
  }
}
  Queue<String> commandList2 = new Queue<>();

  String command = Prompt.inputString("명령> ");
  commandList2.offer(command);

  private static void printCommandHistory(Queue<?> commandList2) {
    try {
      Queue<?> commandQueue = commandList2.clone();
      for (int count = 1; commandQueue.size() > 0; count++) {
        System.out.println(commandQueue.poll());
        
        if ((count % 5) == 0) {
          String response = Prompt.inputString(":");
          if (response.equalsIgnoreCase("q")) {
            break;
          }
        }
      }
    } catch (Exception e) {
      System.out.println("history2 명령 처리 중 오류 발생");
    }
  }