[JAVA/DB] TodoApp (0701-0704)

왕감자·2024년 7월 18일

KB IT's Your Life

목록 보기
91/177

[0701]

V1

프로젝트 세팅

  • 라이브러리 추가 (scoulacli)
  • 롬복(lombok) 설치

settings.gradle

rootProject.name = 'TodoApp'
include ':ScoulaCli'
project(':ScoulaCli').projectDir=new File('C:\\KB_ITs_Your_Life\\Scoula\\ScoulaLib\\scoulacli')

build.gradle

...
dependencies {
    implementation project(':ScoulaCli')
    compileOnly 'org.projectlombok:lombok:1.18.32'
    annotationProcessor 'org.projectlombok:lombok:1.18.32'

    testImplementation platform('org.junit:junit-bom:5.10.0')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}
...

기본 라이브러리에 예외 추가

  • 메뉴 선택 시 발생 가능한 예외
    - NumberFormatException
    - ArrayIndexOutOfBoundsException
    ⇨ 사용자 정의 예외 클래스 BadMenuException 운영

BadMenuException.java

public class BadMenuException extends Exception {
    public BadMenuException() {
        super("잘못된 메뉴 선택"); // super() -> 부모 생서자 호출
    }

    public BadMenuException(String message) {
        super(message);
    }
}

App.java

...
    public void run() {
        init();

        while(true) {
            try {
                menu.printMenu();
                Command command = menu.getSelect(); // 예외 -> BadMenuException로 대체해서 예외 던지기
                command.execute();
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("에러: " + e.getMessage());
            }
        }
    }
...

Menu.java

...
public Command getSelect() throws Exception {
        try {
            int selectNo = Input.getInt("선택> ");
            return menus.get(selectNo - 1).getCommand();
        } catch (Exception e) {
            throw new BadMenuException(); // BadMenuException로 대체해서 예외 던지기
        }
    }
}
...

TodoApp 메뉴

TodoApp.java

public class TodoApp extends App {
    @Override
    public void createMenu(Menu menu) {
        super.createMenu(menu);
        
        // 번호를 입력해도 에러는 나지만 프로그램 종료는 X -> 예외처리 했기 때문
        menu.add(new MenuItem("목록", null)); // Command 객체 아직 준비 x -> Null
        menu.add(new MenuItem("상세", null));
        menu.add(new MenuItem("추가", null));
        menu.add(new MenuItem("수정", null));
        menu.add(new MenuItem("삭제", null)); 
        // 현재는 6.종료만 정상 실행
    }

    public static void main(String[] args) {
        App app = new TodoApp();
        app.run();
    }
}

Todo.java

@NoArgsConstructor // 기본 생성자
@AllArgsConstructor // 얘네를 다 받아줌
@Data // getter, setter 다 포함 ...
public class Todo implements Cloneable{ // 복제(clone)을 허용하는 인터페이스
    private static int gid = 1; // Todo ID 발급을 위한 스태틱 변수 (유일한 정수값), 일종의 ID 발급기^_^

    // 멤버 정의
    private int id;
    private String title;
    private String description;
    private boolean done;
    private Date date;
    
    @Builder // 이 생성자에 준해서 Builder를 운영
    // 호출 할 수 있는 설정: title(), description(), done() / id, date에 대해서는 Building 못함
    public Todo (String title, String description, boolean done) {
        this(gid++, title, description, done, new Date());
    }
}


Dao 클래스

Data Access Object

  • 데이터에 대한 CRUD 처리 담당
    • 데이터 소스(배열, List, DB 등)에 대한 처리를 상위 계층(비지니스 계층)으로 부터 분리

이 데이터로 뭘 할지는 몰라~!
오직 데이터 조작만 담당한다~~!!
비지니스 로직 생각X, 데이터만 바라봐


TodoDao.java

public class TodoDao {
    // 싱글톤 패턴
    private static TodoDao instance = new TodoDao();

    public static TodoDao getInstance() {
        return instance;
    }

    private List<Todo> list; // 리스트를 사용해서 운영

    private TodoDao() {
        list = new ArrayList<>();
        for(int i = 0; i < 10; i++) { // 임시 테스트 구성
            Todo todo = Todo.builder()
                    .title("Todo " + i)
                    .description("Description " + i)
                    .done(false)
                    .build();
            list.add(todo);
        }
    }

    // 리스트에 대한 CRUD
    // C : 추가
    // R : 목록read, 하나read
    // U : 교체
    // D : 리스트에서 제거

    public List<Todo> getList() {
        return list;
    }

    // 단인 element 읽기
    public Todo getTodo(int id) {
        for(Todo todo: list) {
            if(todo.getId() == id) {
                return todo;
            }
        }
        return null;
    }

    // 추가
    public void add(Todo todo) {
        list.add(todo);
    }
    
    // 업데이트
    public void update(Todo todo) {
        for(int i = 0; i < list.size(); i++) {
            if(todo.getId() == list.get(i).getId()) {
                list.set(i, todo);
            }
        }
    }
    
    // 삭제
    public void delete (int id) {
        for(int i = 0; i < list.size(); i++) {
            if(list.get(i).getId() == id) {
                list.remove(i);
                return;
            }
        }
    }
}

[0702]

1) 목록 보기

포맷팅

  • 출력
    • printf(...)
  • 문자열 리턴
    • String line = "%2d] %s".formatted(todo.getId), todo.getTItle());

PrintTodoCommand.java

public class PrintTodoCommand implements Command {
    TodoDao dao = TodoDao.getInstance(); // Dao - 싱글톤으로 운영

    @Override
    public void execute() {
        for (Todo todo: dao.getList()) {
            String line = "%2d] %s".formatted(todo.getId(), todo.getTitle());
            System.out.println(line);
        }
        System.out.println();
    }
}

TodoApp.java

public class TodoApp extends App {
    @Override
    public void createMenu(Menu menu) {
        super.createMenu(menu);

        // 번호를 입력해도 에러는 나지만 프로그램 종료는 X -> 예외처리 했기 때문
        menu.add(new MenuItem("목록", new PrintTodoCommand()));
        menu.add(new MenuItem("상세", null));
        menu.add(new MenuItem("추가", null));
        menu.add(new MenuItem("수정", null));
        menu.add(new MenuItem("삭제", null));
        // 현재는 6.종료만 정상 실행
    }

    public static void main(String[] args) {
        App app = new TodoApp();
        app.run();
    }
}


2) 상세 보기

SimpleDateFormat
특정 문자열 포맷으로 얻기 (java.text.SimpleDateFormat 클래스)
ex) 2024-06-27 15:27:41

Todo.java

public class Todo implements Cloneable{ // 복제(clone)을 허용하는 인터페이스
    private static int gid = 1; // Todo ID 발급을 위한 스태틱 변수 (유일한 정수값), 일종의 ID 발급기^_^

    // 멤버 정의
    private int id;
    private String title;
    private String description;
    private boolean done;
    private Date date;

    @Builder // 생성자 레벨에 붙인 Builder -> 이 생성자에 준해서 Builder를 운영
    // 호출 할 수 있는 설정: title(), description(), done() / id, date에 대해서는 Building 못함
    public Todo (String title, String description, boolean done) {
        this(gid++, title, description, done, new Date());
    }

    @Override
    public Object clone() { // 부모에서는 protect 였던 것을 public으로 변경
         try {
            return super.clone();
         } catch (CloneNotSupportedException e) {
             throw new RuntimeException(e);
         }
    }
    
    // 메서드 추가
    // 문자열로 날짜를 리턴
    public String getRegDate() {
        SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 패턴 문자열
        return sdf.format(date);
    }
}

DetailTodoCommand.java

public class DetailTodoCommand implements Command {
    TodoDao dao = TodoDao.getInstance(); // Dao

    @Override
    public void execute() {
        int id = Input.getInt("Todo Id: "); // 에러1) id 입력 받을 때 - 숫자가 아닌 것을 입력했을 때
        Todo todo = dao.getTodo(id); // 에러2) 존재하지 않는 id를 입력 했을 때 - NullPointException 발생

        System.out.println("[Todo 상세보기]-----------------------");
        System.out.println("ID     : " + todo.getId());
        System.out.println("제목    : " + todo.getTitle());
        System.out.println("설명    : " + todo.getDescription());
        System.out.println("완료여부 : " + todo.isDone());
        System.out.println("등록일   : " + todo.getRegDate());
        System.out.println("------------------------------------");
        System.out.println();
    }
}

TodoApp.java

public class TodoApp extends App {
    @Override
    public void createMenu(Menu menu) {
        super.createMenu(menu);

        // 번호를 입력해도 에러는 나지만 프로그램 종료는 X -> 예외처리 했기 때문
        menu.add(new MenuItem("목록", new PrintTodoCommand()));
        menu.add(new MenuItem("상세", new DetailTodoCommand()));
        menu.add(new MenuItem("추가", null));
        menu.add(new MenuItem("수정", null));
        menu.add(new MenuItem("삭제", null));
        // 현재는 6.종료만 정상 실행
    }

    public static void main(String[] args) {
        App app = new TodoApp();
        app.run();
    }
}


3) 삭제

DeleteTodoCommand.java

// 삭제
public class DeleteTodoCommand implements Command {
    TodoDao dao = TodoDao.getInstance();

    @Override
    public void execute() {
        int id = Input.getInt("삭제할 Todo Id: ");
        dao.delete(id);
        System.out.println();
    }
}

TodoApp.java

public class TodoApp extends App {
    @Override
    public void createMenu(Menu menu) {
        super.createMenu(menu);

        // 번호를 입력해도 에러는 나지만 프로그램 종료는 X -> 예외처리 했기 때문
        menu.add(new MenuItem("목록", new PrintTodoCommand()));
        menu.add(new MenuItem("상세", new DetailTodoCommand()));
        menu.add(new MenuItem("추가", null));
        menu.add(new MenuItem("수정", null));
        menu.add(new MenuItem("삭제", new DeleteTodoCommand()));
        // 현재는 6.종료만 정상 실행
    }

    public static void main(String[] args) {
        App app = new TodoApp();
        app.run();
    }
}

[0703]

4) 추가

Input.java

package org.scoula.lib.cli.ui;

public class Input {
    static Scanner scanner = new Scanner(System.in);

    public static int getInt(String title) {
        System.out.print(title);
        return Integer.parseInt(scanner.nextLine());
    }
    
    // String 추가
    public static String getLine(String title) {
        System.out.print(title);
        return scanner.nextLine();
    }
}

AddTodoCommand.java

public class AddTodoCommand implements Command {
    TodoDao dao = TodoDao.getInstance();

    @Override
    public void execute() {
        System.out.println("[새 Todo 추가]-----------------------");
        String title = Input.getLine("제목: ");
        String description = Input.getLine("설명: ");
        System.out.println("------------------------------------");

        Todo todo = Todo.builder()
                .title(title)
                .description(description)
                .done(false)
                .build();

        dao.add(todo);

        System.out.println();
    }
}

TodoApp.java

public class TodoApp extends App {
    @Override
    public void createMenu(Menu menu) {
        super.createMenu(menu);

        // 번호를 입력해도 에러는 나지만 프로그램 종료는 X -> 예외처리 했기 때문
        menu.add(new MenuItem("목록", new PrintTodoCommand()));
        menu.add(new MenuItem("상세", new DetailTodoCommand()));
        menu.add(new MenuItem("추가", new AddTodoCommand()));
        menu.add(new MenuItem("수정", null));
        menu.add(new MenuItem("삭제", new DeleteTodoCommand()));
        // 6. 프로그램 종료
    }

    public static void main(String[] args) {
        App app = new TodoApp();
        app.run();
    }
}


5) 수정

CLI에서 가장 힘든 작업~~ 수정~~
CLI 특성상 기존 데이터 수정 어려움

Input.java

package org.scoula.lib.cli.ui;

public class Input {
    static Scanner scanner = new Scanner(System.in);

    public static int getInt(String title) {
        System.out.print(title);
        return Integer.parseInt(scanner.nextLine());
    }

    // getLine 수정
    public static String getLine(String title, String defaultValue) {
        // 이름(김길동):
        System.out.printf("%s(%s): ", title, defaultValue);
        String answer = scanner.nextLine();

        // 그냥 엔터 쳤으면 defaultValue 리턴, 입력값 있으면 answer 리턴
        return answer.isEmpty() ? defaultValue : answer;
    }

    // boolean 입력 (완료 여부) 
    // 타이틀만 주었을 경우 - true(Default)
    public static boolean confirm(String title) {
        return confirm(title, true); // 아래 confirm 메서드
    }
    // false
    public static boolean confirm(String title, boolean defaultValue) {
        String yesNo = defaultValue ? "(Y/n)" : "(y/N)";
        System.out.printf("%s %s: ", title, yesNo);

        String answer = scanner.nextLine();
        if (answer.isEmpty())
            return defaultValue;

        return answer.equalsIgnoreCase("y"); // IgnoreCase: 대소문자 구분 없이 비교
    }
}


UpdateTodoCommand.java

public class UpdateTodoCommand implements Command {
    TodoDao dao = TodoDao.getInstance();

    @Override
    public void execute() {
        int id = Input.getInt("수정할 Id: ");
        Todo todo = dao.getTodo(id);

        System.out.println("[Todo 수정하기]-----------------------");
        System.out.println("ID   : " + todo.getId());
        String title = Input.getLine("제목", todo.getTitle());
        String description = Input.getLine("설명", todo.getDescription());
        Boolean done = Input.confirm("완료여부", todo.isDone());
        System.out.println("------------------------------------");
        System.out.println();

        // 인스턴스 복제 : clone() 메서드 in Object
        // Cloneable 인터페이스가 붙어야 clone 메서드 사용 가능
        Todo updateTodo = (Todo)todo.clone();
        updateTodo.setTitle(title);
        updateTodo.setDescription(description);
        updateTodo.setDone(done);

        dao.update(updateTodo);
    }
}

Todo.java

@Override
    // 부모에서는 protect 였던 것을 public으로 변경
    // return은 Object
    // 실제 사용시에는 down casting 해야 함
    // 반드시 Cloneable 인스턴스를 구현한 객체만 clone 호출 가능
    public Object clone() {
         try {
            return super.clone();
         } catch (CloneNotSupportedException e) {
             throw new RuntimeException(e);
         }
    }

TodoApp.java

public class TodoApp extends App {
    @Override
    public void createMenu(Menu menu) {
        super.createMenu(menu);

        // 번호를 입력해도 에러는 나지만 프로그램 종료는 X -> 예외처리 했기 때문
        menu.add(new MenuItem("목록", new PrintTodoCommand()));
        menu.add(new MenuItem("상세", new DetailTodoCommand()));
        menu.add(new MenuItem("추가", new AddTodoCommand()));
        menu.add(new MenuItem("수정", new UpdateTodoCommand()));
        menu.add(new MenuItem("삭제", new DeleteTodoCommand()));
        // 6. 프로그램 종료
    }

    public static void main(String[] args) {
        App app = new TodoApp();
        app.run();
    }
}

[0704]

TodoDao 추상화

  • 현재 Dao> ArrayList로 운영
    • Dao 교체 > Database로 운영 ❓ ➞ Command 객체에 수정 사항 발생 (OCP 위배)
      • ∴ 인터페이스 타입으로 운영해줘야함 (Dao를 인터페이스로 추상화)

이미 구현체 有 ⇨ 그 구현체를 기반으로 자동으로 인터페이스 뽑아내기~!

1) 기존 TodoDao 이름 변경 ➞ TodoListDao

2) 인터페이스 추출
Refactor - Extracl/Introduce - Interface
인터페이스 이름: TodoDao

package org.scoula.todo.dao;

import org.scoula.todo.domain.Todo;

import java.util.List;

// TodoDao 인터페이스 자동 생성 완
public interface TodoDao {
    List<Todo> getList();

    // 단일 element 읽기
    Todo getTodo(int id);

    // 추가
    void add(Todo todo);

    // 업데이트
    void update(Todo todo);

    // 삭제
    void delete(int id);
}

TodoListDao에 구현 파트가 자동으로 추가
메소드 앞에 오버라이드 어노테이션이 자동으로 붙음


3) getInstance 리턴 타입 TodoDao로 변경
4) 커맨드 - TodoListDao ➞ TodoDao 인터페이스 타입으로 변경
TodoDao dao = TodoListDao.getInstance();

∴ Dao가 다른 걸로 교체되더라도 수정사항은 TodoListDao 뿐~!



V2

  • TodoService
    - Todo와 관련된 Command들을 캡슐화
    - 메서드 참조를 이용해 MenuItem에 설정

TodoService.java

// 캡슐화
public class TodoService {
    TodoDao dao = TodoListDao.getInstance();

    // Command들이 다 메서드로 들어옴
    public void printTodoList() {
        for (Todo todo: dao.getList()) {
            String line = "%2d] %s".formatted(todo.getId(), todo.getTitle());
            System.out.println(line);
        }
        System.out.println();
    }

    public void detailTodo() {
        int id = Input.getInt("Todo Id: ");
        Todo todo = dao.getTodo(id);

        System.out.println("[Todo 상세보기]-----------------------");
        System.out.println("ID     : " + todo.getId());
        System.out.println("제목    : " + todo.getTitle());
        System.out.println("설명    : " + todo.getDescription());
        System.out.println("완료여부 : " + todo.isDone());
        System.out.println("등록일   : " + todo.getRegDate());
        System.out.println("------------------------------------");
        System.out.println();
    }

    public void addTodo() {
        System.out.println("[새 Todo 추가]-----------------------");
        String title = Input.getLine("제목: ");
        String description = Input.getLine("설명: ");
        System.out.println("------------------------------------");

        Todo todo = Todo.builder()
                .title(title)
                .description(description)
                .done(false)
                .build();

        dao.add(todo);

        System.out.println();
    }

    public void updateTodo() {
        int id = Input.getInt("수정할 Id: ");
        Todo todo = dao.getTodo(id);

        System.out.println("[Todo 수정하기]-----------------------");
        System.out.println("ID   : " + todo.getId());
        String title = Input.getLine("제목", todo.getTitle());
        String description = Input.getLine("설명", todo.getDescription());
        Boolean done = Input.confirm("완료여부", todo.isDone());
        System.out.println("------------------------------------");
        System.out.println();

        // 인스턴스 복제 : clone() 메서드 in Object
        // Cloneable 인터페이스가 붙어야 clone 메서드 사용 가능
        Todo updateTodo = (Todo)todo.clone();
        updateTodo.setTitle(title);
        updateTodo.setDescription(description);
        updateTodo.setDone(done);

        dao.update(updateTodo);
    }

    public void deleteTodo() {
        int id = Input.getInt("삭제할 Todo Id: ");
        dao.delete(id);
        System.out.println();
    }
}

(기존: TodoApp_v1.java)

TodoApp.java

public class TodoApp extends App {
    TodoService service = new TodoService();

    @Override
    public void createMenu(Menu menu) {
        super.createMenu(menu);

        // 메서드 참조로 대체 (service 객체 인스텐스의 메서드 사용~!)
        menu.add(new MenuItem("목록", service::printTodoList));
        menu.add(new MenuItem("상세", service::detailTodo));
        menu.add(new MenuItem("추가", service::addTodo));
        menu.add(new MenuItem("수정", service::updateTodo));
        menu.add(new MenuItem("삭제", service::deleteTodo));
        // 6. 프로그램 종료
    }

    public static void main(String[] args) {
        App app = new TodoApp();
        app.run();
    }
}



✔새로운 개념

Command: Functional Interface
➞ 람다식, 메서드 참조로 대체 가능

인스턴스 복제

  • clone() 메서드 (in Object)
    • Cloneable 인터페이스를 구현한 객체만 호출 가능
    • protect로 정의 되어 있음 ⇨ overiding 재정의 해야 함
    • return은 Object
    • 실제 사용 시 down casting

0개의 댓글