handler.AbstractHandler 추상 클래스 생성
메뉴 목록을 담고 있는 배열 menus는 static이 아닌, instance로 만들어 준다.
→ board와 member는 서로 다른 메뉴를 가져야 하는데, static으로 선언하면 하나의 메뉴를 공유해야하기 때문이다.
⇒ 각 핸들러 클래스에서 반드시 메뉴를 각각 초기화 시키기 위하여 생성자를 이용한다.
핸들러에서 사용할 메서드들은 공개로는 설정하진 않지만, 서브 클래스들인 핸들러에서 재정의, 오버라이딩 기회를 주기 위하여 공개 범위를 protected로 설정한다.
→ 이것이 protected를 사용하는 이유이다.
execute() 메소드 같은 경우는 BoardHandler와 MemberHandler 사이에서 공통된 부분은 그대로 두고 서로 다른 부분인 메뉴 출력, 즉 상세한 부분은 service라는 추상메서드를 선언하여 각자에 맞게 재정의하도록 한다.
⇒ service 추상 메서드 선언
템플릿 메서드 패턴(디자인) 적용: 수퍼클래스에서 틀(템플릿) 안에 들어가는 세부적인 내용을 서브 클래스에서 구체화한다.
⇒ 전체적인 흐름, 틀 잡기는 추상 클래스에서, 자세한 흐름은 서브 클래스에서 구현한다.
템플릿 메서드 패턴(template method pattern)
- 수퍼 클래스의 execute()에서 동작의 전체적인 흐름을 정의하고(틀을 만들고),
- 서브 클래스의 service()에서 동작을 구제척으로 정의한다.(세부적인 항목을 구현한다)
AbstractHandler class
package com.bitcamp.handler;
import com.bitcamp.board.App;
import com.bitcamp.util.Prompt;
// Handler 규격에 맞추어 핸들러의 서브 클래스에게 물려줄 공통 필드가 메서드를 구현한다.
public abstract class AbstractHandler implements Handler {
// 같은 타입의 (board, member)핸들러가 사용할 메뉴 목록을 담을 배열을 준비한다.
// => 메뉴 목록은 생성자를 통해 초기화시킨다.
//
private String [] menus ;
// 반드시 메뉴 목록을 초기화 시키도록 강제하기 위해
// 기본 생성자 대신 메뉴 목록을 배열로 받는 생성자를 준비한다.
public AbstractHandler(String[] menus) {
this.menus = menus;
}
// static이 아니라, 인스터스 멤버를 사용하려고 인스턴스 메서드로 변경
// 다음 메서드는 execute()에서 메뉴를 출력할 때 사용된다.
// 다만 서브 클래스에서 출력 형식을 바꾸기 위해 오버라이딩 할 수 있도록
// 접근 범위를 protected로 설정한다. -> 공개하는 메서드는 아니지만, 서브 클래스 중에서 재정의 기회를 주고 싶을 때 사용
protected void printMenus() {
for(int i=0;i<menus.length; i++) {
System.out.printf(" %d: %s\n", i + 1, menus[i]);
}
printBlankLine();
}
protected static void printHeadline() {
System.out.println("=========================================");
}
protected static void printBlankLine() {
System.out.println(); // 메뉴를 처리한 후 빈 줄 출력
}
protected static void printTitle() {
System.out.printf("%s:\n", App.breadcrumbMenu);
}
@Override
public void execute() {
// TODO Auto-generated method stub
while (true) {
printTitle();
printMenus();
try {
int menuNo = Prompt.inputInt(String.format(" 메뉴를 선택하세요[1..%d](0: 이전) ", menus.length));
if(menuNo > 0 && menuNo <= menus.length) {
//메뉴에 진입할 때 breadCrumb메뉴바에 그 메뉴를 등록한다.
App.breadcrumbMenu.push(menus[menuNo-1]);
} else if(menuNo == 0){
return; // 메인 메뉴로 돌아간다.
}else {
System.out.println("메뉴 번호가 옳지 않습니다!");
continue; // while문의 조건 검사로 보낸다.
}
printHeadline();
// 서브 메뉴의 제목을 출력한다.
printTitle();
// 사용자가 입력한 메뉴 번호에 대해 작업을 수행한다! 원래 switch 문이었던 것
service(menuNo);
printBlankLine();
App.breadcrumbMenu.pop();
} catch (Exception ex) {
System.out.printf("예외 발생: %s\n", ex.getMessage());
}
} // while
}
// 서브 클래스가 반드시 만들어야 할 메서드
// => 메뉴 번호를 받으면 그 메뉴에 해당하는 작업을수행한다.
// 서브 클래스에게 구현을 강제하기 위해 추상 메서드로 선언한다. => 추상 메서드의 목표!
public abstract void service(int menuNo);
}
* 게시글 메뉴 처리 클래스
*/
package com.bitcamp.board.handler;
import java.text.SimpleDateFormat;
import java.util.Date;
import com.bitcamp.board.dao.BoardDao;
import com.bitcamp.board.domain.Board;
import com.bitcamp.handler.AbstractHandler;
import com.bitcamp.util.Prompt;
public class BoardHandler extends AbstractHandler{
// 게시글 목록을 관리할 객체 준비
private BoardDao boardDao = new BoardDao();
public BoardHandler() {
// 수퍼 클래스의 생성자를 호출 할 때 메뉴 목록을 전달한다. super class constructor!!!!!!!! super!!!!!
super(new String[] {"목록", "상세보기", "등록", "삭제", "변경"});
// 변수를 선언과 동시에 넣을 때는 new String[]을 생략할 수 있지만, 나중에 집어 넣을 때는 생략할수 없다.
}
// 템플릿 메서드 디자인 패턴, 템플릿(틀) -> 템플릿 메서드 패턴(template method pattern)
// 수퍼클래스의 execute()에서 동작의 전체적인 흐름을 정의하고(틀을 만들고),
// 서브클래스의 service()에서 구체적인 동작을 정의한다. (세부적인 항목을 구현한다.)
@Override
public void service(int menuNo) {
// TODO Auto-generated method stub
switch (menuNo) {
case 1:
this.onList(); break;
case 2: this.onDetail(); break;
case 3: this.onInput(); break;
case 4: this.onDelete(); break;
case 5: this.onUpdate(); break;
default:
}
}
private void onList() {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
System.out.println("번호 제목 조회수 작성자 등록일");
Board[] boards = this.boardDao.findAll();
for (Board board : boards) {
Date date = new Date(board.createdDate);
String dateStr = formatter.format(date);
System.out.printf("%d\t%s\t%d\t%s\t%s\n",
board.no, board.title, board.viewCount, board.writer, dateStr);
}
}
private void onDetail() {
int boardNo = 0;
while (true) {
try {
boardNo = Prompt.inputInt("조회할 게시글 번호? ");
break;
} catch (Exception ex) {
System.out.println("입력 값이 옳지 않습니다!");
}
}
// 해당 번호의 게시글이 몇 번 배열에 들어 있는지 알아내기
Board board = this.boardDao.findByNo(boardNo);
// 사용자가 입력한 번호에 해당하는 게시글을 못 찾았다면
if (board == null) {
System.out.println("해당 번호의 게시글이 없습니다!");
return;
}
System.out.printf("번호: %d\n", board.no);
System.out.printf("제목: %s\n", board.title);
System.out.printf("내용: %s\n", board.content);
System.out.printf("조회수: %d\n", board.viewCount);
System.out.printf("작성자: %s\n", board.writer);
Date date = new Date(board.createdDate);
}
private void onInput() {
Board board = new Board();
board.title = Prompt.inputString("제목? ");
board.content = Prompt.inputString("내용? ");
board.writer = Prompt.inputString("작성자? ");
board.password = Prompt.inputString("암호? ");
board.viewCount = 0;
board.createdDate = System.currentTimeMillis();
this.boardDao.insert(board);
System.out.println("게시글을 등록했습니다.");
}
private void onDelete() {
int boardNo = 0;
while (true) {
try {
boardNo = Prompt.inputInt("삭제할 게시글 번호? ");
break;
} catch (Exception ex) {
System.out.println("입력 값이 옳지 않습니다!");
}
}
if (boardDao.delete(boardNo)) {
System.out.println("삭제하였습니다.");
} else {
System.out.println("해당 번호의 게시글이 없습니다!");
}
}
private void onUpdate() {
int boardNo = 0;
while (true) {
try {
boardNo = Prompt.inputInt("변경할 게시글 번호? ");
break;
} catch (Throwable ex) {
System.out.println("입력 값이 옳지 않습니다!");
}
}
Board board = this.boardDao.findByNo(boardNo);
if (board == null) {
System.out.println("해당 번호의 게시글이 없습니다!");
return;
}
String newTitle = Prompt.inputString("제목?(" + board.title + ") ");
String newContent = Prompt.inputString(String.format("내용?(%s) ", board.content));
String input = Prompt.inputString("변경하시겠습니까?(y/n) ");
if (input.equals("y")) {
board.title = newTitle;
board.content = newContent;
System.out.println("변경했습니다.");
} else {
System.out.println("변경 취소했습니다.");
}
}
}
/*
* 회원 메뉴 처리 클래스
*/
package com.bitcamp.board.handler;
import java.util.Date;
import com.bitcamp.board.dao.MemberDao;
import com.bitcamp.board.domain.Member;
import com.bitcamp.handler.AbstractHandler;
import com.bitcamp.util.Prompt;
public class MemberHandler extends AbstractHandler{
private MemberDao memberDao = new MemberDao();
public MemberHandler() {
super(new String[] {"목록", "상세보기", "등록", "삭제", "변경"});
}
static void printMenus(String[] menus) {
for(int i=0;i<menus.length; i++) {
System.out.printf(" %d: %s\n", i + 1, menus[i]);
}
System.out.println();
}
@Override
public void service(int menuNo) {
// 서브 메뉴의 제목을 출력한다.
switch (menuNo) {
case 1: this.onList(); break;
case 2: this.onDetail(); break;
case 3: this.onInput(); break;
case 4: this.onDelete(); break;
case 5: this.onUpdate(); break;
default: System.out.println("메뉴 번호가 옳지 않습니다!");
}
}
private void onList() {
System.out.println("이메일 이름");
Member[] members = this.memberDao.findAll();
for (Member member : members) {
System.out.printf("%s\t%s\n",
member.email, member.name);
}
}
private void onDetail() {
String email = Prompt.inputString("조회할 회원 이메일? ");
Member member = this.memberDao.findByEmail(email);
if (member == null) {
System.out.println("해당 이메일의 회원이 없습니다!");
return;
}
System.out.printf("이름: %s\n", member.name);
System.out.printf("이메일: %s\n", member.email);
Date date = new Date(member.createdDate);
System.out.printf("등록일: %tY-%1$tm-%1$td %1$tH:%1$tM\n", date);
}
private void onInput() {
Member member = new Member();
member.name = Prompt.inputString("이름? ");
member.email = Prompt.inputString("이메일? ");
member.password = Prompt.inputString("암호? ");
member.createdDate = System.currentTimeMillis();
this.memberDao.insert(member);
System.out.println("회원을 등록했습니다.");
}
private void onDelete() {
String email = Prompt.inputString("삭제할 회원 이메일? ");
if (memberDao.delete(email)) {
System.out.println("삭제하였습니다.");
} else {
System.out.println("해당 이메일의 회원이 없습니다!");
}
}
private void onUpdate() {
String email = Prompt.inputString("변경할 회원 이메일? ");
Member member = this.memberDao.findByEmail(email);
if (member == null) {
System.out.println("해당 이메일의 회원이 없습니다!");
return;
}
String newName = Prompt.inputString("이름?(" + member.name + ") ");
String newEmail = Prompt.inputString(String.format("이메일?(%s) ", member.email));
String input = Prompt.inputString("변경하시겠습니까?(y/n) ");
if (input.equals("y")) {
member.name = newName;
member.email = newEmail;
System.out.println("변경했습니다.");
} else {
System.out.println("변경 취소했습니다.");
}
}
}
Object obj; → 가능
obj = new String(); → 가능
obj = new File(); → 가능
obj = new Date(); → 가능
E obj; // E = 타입 미지정 = 타입을 가리키는 변수 = 타입 파라미터
obj = ? // E가 가리키는 타입이 무엇이냐에 따라 저장할 수 있는 값이 결정된다.
제네릭 사용 전
(1) 각 데이터 타입마다 클래스 정의
→ 유지보수 거의 불가!
(2) Object 클래스
→ 클래스를 한 개만 정의해서 좋다.
→ 하지만 사용이 불편(특정 타입만 저장X, 꺼낼 때마다 형 변환 필요!)
제네릭 사용 후
제네릭 문법이란?
한 개의 클래스를 만들어 놓고, 각 타입 별로 따로 만든 클래스인 것처럼 사용할 수 있게 해주는 문법
ObjectList<E> list = new ObjectList<>();
배열의 타입과 배열 항목의 타입은 다르다.
package com.bitcamp.util;
/**
* 인덱스를 기반으로 목록을 다루는 메서드의 규격을 정한다.
* @author vivi
*
*/
// List에서 다루는 항목의 타입을 외부에서 받을 수 있도록
// 타입 파라미터를 선언한다.
public interface List<E> {
// E 가 가리키는 타입은 List를 사용하는 시점에 결정된다.
void add(E value);
E get(int index);
E remove(int index);
Object[] toArray();
E[] toArray(E[] arr);
int size();
}
public abstract class AbstractList<E> implements List<E> {
protected int size;
@Override
public int size() {
return size;
}
}
ObjectList class
Object로 선언되어 있던 것들을 모두 제너릭 타입으로 넘겨받은 E로 변경해준다.
다만, 제네릭 배열은 제공이 안되기 때문에 Object인 상태로 남겨둔다.
따라서 또 다른 toArray() 메서드, E[] toArray(E[])을 오버라이딩해서 만들어준다.
LinkedList<String> list3 = new LinkedList<>();
list3.add("홍길동");
list3.add("임꺽정");
list3.add("유관순");
list3.add("안중근");
// String[] names = new String[list3.size()];
// list3.toArray(names);
String[] names = list3.toArray(new String[0]);
for (String name : names) {
System.out.println(name);
}
```
만약 배열의 크기가 충분치 않아 배열을 새로 초기화 해줄 때 다음의 코드를 이용하여 배열을 만들어준다.
(E[])Array.newInstance(레퍼런스 배열의 항목 타입, 배열의 개수)
⇒ (E[])Array.newInstance(arr.getClass().getComponentType(), size)
@SuppressWarnings("unchecked")
@Override
public E[] toArray(E[] arr) {
// 파라미터로 받은 배열이 목록에 저장된 항목의 개수 보다 작다면
if (arr.length < size) {
// 파라미터로 받은 배열과 똑같은 타입의 배열을 만든다.
// 단 크기는 size 에 지정한 개수 만큼 만든다.
arr = (E[]) Array.newInstance(
arr.getClass().getComponentType(), size);
}
// 목록에 있는 항목들을 파라미터로 받은 배열에 형변환을 하며 복사한다.
for (int i = 0; i < size; i++) {
arr[i] = (E) elementData[i];
}
return arr;
}
package com.bitcamp.util;
import java.lang.reflect.Array;
public class ObjectList<E> extends AbstractList<E> {
private static final int DEFAULT_CAPACITY = 10;
private Object[] elementData;
public ObjectList() {
elementData = new Object[DEFAULT_CAPACITY];
}
public ObjectList(int initialCapacity) {
elementData = new Object[initialCapacity];
}
@Override // 인터페이스 규격에 따라 메서드를 정의하는 것도 오버라이딩으로 간주한다.
public void add(E e) {
if (size == elementData.length) {
grow();
}
elementData[size++] = e;
}
@Override
public Object[] toArray() {
Object[] arr = new Object[size];
for (int i = 0; i < arr.length; i++) {
arr[i] = elementData[i];
}
return arr;
}
@SuppressWarnings("unchecked")
@Override
public E[] toArray(E[] arr) {
// 파라미터로 받은 배열이 목록에 저장된 항목의 개수 보다 작다면
if (arr.length < size) {
// 파라미터로 받은 배열과 똑같은 타입의 배열을 만든다.
// 단 크기는 size 에 지정한 개수 만큼 만든다.
arr = (E[]) Array.newInstance(
arr.getClass().getComponentType(), // 레퍼런스 배열의 항목 타입
size /* 배열의 개수*/);
}
// 목록에 있는 항목들을 파라미터로 받은 배열에 복사한다.
for (int i = 0; i < size; i++) {
arr[i] = (E) elementData[i];
}
return arr;
}
@SuppressWarnings("unchecked")
@Override
public E get(int index) {
if (index < 0 || index >= size) {
throw new ListException("인덱스가 무효함!");
}
return (E) elementData[index];
}
@SuppressWarnings("unchecked")
@Override
public E remove(int index) {
if (index < 0 || index >= size) {
throw new ListException("인덱스가 무효합니다!");
}
// 삭제한 객체를 리턴할 수 있도록 임시 변수에 담아 둔다.
E deleted = (E) elementData[index];
for (int i = index + 1; i < size; i++) {
elementData[i - 1] = elementData[i];
}
elementData[--size] = null;
return deleted;
}
private void grow() {
int newCapacity = elementData.length + (elementData.length >> 1);
Object[] newArray = new Object[newCapacity];
for (int i = 0; i < elementData.length; i++) {
newArray[i] = elementData[i];
}
elementData = newArray;
}
}
package com.bitcamp.util;
import java.lang.reflect.Array;
public class LinkedList<E> extends AbstractList<E> {
private Node<E> head; // 첫 노드의 주소를 저장
private Node<E> tail; // 마지막 노드의 주소를 저장
@Override
public void add(E value) {
Node<E> node = new Node<>(value);
size++; // 목록의 크기를 한 개 증가시킨다.
if (tail == null) {
head = tail = node; // 첫 노드를 등록한다.
return;
}
tail.next = node; // 리스트 끝에 새 노드를 연결한다.
node.prev = tail; // 새 노드가 현재의 끝 노드를 가리키게 한다.
tail = node; // 새 노드를 끝 노드로 만든다.
}
@Override
public E get(int index) {
// 인덱스의 유효 여부 검사
if (index < 0 || index >= size) {
throw new ListException("인덱스의 범위를 초과했습니다!");
}
// 인덱스에 해당하는 노드를 찾을 때 head 부터 시작한다.
Node<E> cursor = head;
// 지정된 인덱스의 노드 주소를 알아낸다.
for (int i = 0; i < index; i++) {
cursor = cursor.next;
}
// cursor가 가리키는 노드의 값을 꺼내 리턴한다.
return cursor.value;
}
@Override
public E remove(int index) {
// 인덱스의 유효 여부 검사
if (index < 0 || index >= size) {
throw new ListException("인덱스의 범위를 초과했습니다!");
}
// 목록 크기를 한 개 줄인다.
size--;
// 삭제할 값을 임시 보관하여 메서드를 리턴할 때 호출자에게 전달한다.
E deleted;
if (head == tail) { // 마지막 남은 노드를 제거할 때
deleted = head.value; // 노드를 삭제하기 전에 리턴할 수 있도록 값을 임시 보관한다.
head.value = null; // 노드에 들어 있는 값 객체의 주소를 비운다.
head = tail = null;
return deleted; // 메서드를 종료할 때 호출자에게 삭제한 값을 리턴한다.
}
// 삭제할 노드를 찾기 위해 시작 노드를 head로 설정한다.
Node<E> cursor = head;
// 지정된 인덱스의 노드 주소를 알아낸다.
for (int i = 0; i < index; i++) {
cursor = cursor.next;
}
// 찾은 노드의 앞, 뒤 노드를 바로 연결한다.
if (cursor.prev != null) { // 맨 앞 노드가 아니라면
cursor.prev.next = cursor.next; // 현재 노드의 다음 노드 주소를 이전 노드의 next 저장
} else { // 맨 앞 노드라면
head = cursor.next; // 삭제할 다음 노드를 시작 노드로 설정한다.
head.prev = null; // 시작 노드이기에 앞노드를 가리키지 않게 한다.
}
if (cursor.next != null) { // 마지막 노드가 아니라면
cursor.next.prev = cursor.prev; // 현재 노드의 이전 노드 주소를 다음 노드의 prev 저장
} else { // 마지막 노드라면
tail = cursor.prev; // 현재 커서의 이전 노드를 마지막 노드로 설정한다.
tail.next = null; // 마지막 노드이기에 다음 노드를 가리키지 않게 한다.
}
// 삭제할 노드를 초기화시킨다.
// => garbage 객체가 다른 garbage 객체를 참조하지 않게 한다.
deleted = cursor.value; // 노드를 삭제하기 전에 노드에 들어 있는 값을 임시 보관해 둔다.
cursor.value = null;
cursor.prev = null;
cursor.next = null;
return deleted; // 메서드를 리턴할 때 삭제된 값을 호출자에게 전달한다.
}
@Override
public Object[] toArray() {
// 값을 담을 배열을 준비
Object[] arr = new Object[size];
// 노드를 따라 가면서 값을 꺼내 배열에 담는다.
Node<E> cursor = head;
for (int i = 0; i < size; i++) {
arr[i] = cursor.value;
cursor = cursor.next;
}
return arr;
}
@SuppressWarnings("unchecked")
@Override
public E[] toArray(E[] arr) {
if (arr.length < size) {
arr = (E[]) Array.newInstance(
arr.getClass().getComponentType(), // 레퍼런스 배열의 항목 타입
size /* 배열의 개수*/);
}
// 노드를 따라 가면서 값을 꺼내 배열에 담는다.
Node<E> cursor = head;
for (int i = 0; i < size; i++) {
arr[i] = cursor.value;
cursor = cursor.next;
}
return arr;
}
private static class Node<T> {
T value;
Node<T> prev;
Node<T> next;
public Node(T v) {
this.value = v;
}
}
} // LinkedList 끝
dao.BoardDao 클래스 변경
dao.MemberDao 클래스 변경
BoardDao class
List<Board> list = new LinkedList<>();
public Board[] findAll() {
return list.toArray(new Board[0]);
}
package com.bitcamp.board.dao;
import com.bitcamp.board.domain.Board;
import com.bitcamp.util.LinkedList;
import com.bitcamp.util.List;
// 게시글 목록을 관리하는 역할
//
public class BoardDao {
List<Board> list = new LinkedList<>();
private int boardNo = 0;
public void insert(Board board) {
board.no = nextNo();
list.add(board);
}
public Board findByNo(int boardNo) {
for (int i = 0; i < list.size(); i++) {
Board board = list.get(i);
if (board.no == boardNo) {
return board;
}
}
return null;
}
public boolean delete(int boardNo) {
for (int i = 0; i < list.size(); i++) {
Board board = list.get(i);
if (board.no == boardNo) {
return list.remove(i) != null;
}
}
return false;
}
public Board[] findAll() {
return list.toArray(new Board[0]);
}
private int nextNo() {
return ++boardNo;
}
}
package com.bitcamp.board.dao;
import com.bitcamp.board.domain.Member;
import com.bitcamp.util.LinkedList;
import com.bitcamp.util.List;
// 회원 목록을 관리하는 역할
//
public class MemberDao {
List<Member> list = new LinkedList<Member>();
public void insert(Member member) {
list.add(member);
}
public Member findByEmail(String email) {
for (int i = 0; i < list.size(); i++) {
Member member = list.get(i);
if (member.email.equals(email)) {
return member;
}
}
return null;
}
public boolean delete(String email) {
for (int i = 0; i < list.size(); i++) {
Member member = list.get(i);
if (member.email.equals(email)) {
return list.remove(i) != null;
}
}
return false;
}
public Member[] findAll() {
return list.toArray(new Member[0]);
}
}