✅ Java Day7

YDC·2025년 6월 14일

우테코8기

목록 보기
7/23

🔁 Day 6 복습

chartAt() 문자열에서 ?번째를 가져오는 함수

문자를 숫자로 바꾸는 방법

'숫자' - '0' 아스키 코드 연산

Character.getNumericValue()

문자 숫자뿐 아니라 특수 문자도 숫자로 바꿀 수 있음

Integer.parseInt(String)

문자열 전체를 정수로 바꾸는 메소드

문자열이 숫자일 때만 가능

🧯 try - catch - finally란?

예외 처리를 하려고 try 해보고, 안되면 catch 로 가서 처리
처리 여부와 관계없이 finally 실행하는 구조

🚨 예외 처리 테스트를 하는 이유

예외 처리가 안되면 시스템 자체가 멈춰버리기 때문에
예외가 제대로 처리되는지 확인이 필요함

🧱 Entity 는 데이터를 표현하는 명사 역할의 클래스

⚙️ Service 는 Entity를 활용해서 동작(로직)을 수행하는 클래스 — 동사 역할 클래스

🧰 isValidIndex() 같은 유틸리티 메서드의 의미

private boolean isValidIndex(int index) {
    return index >= 0 && index < todos.size();
}

이런 함수는 코드 재사용성도 높이고,
가독성, 안정성까지 챙기는 구조적인 코드 스타일

🎯 오늘 학습 목표

📌 기능 확장

마감일 기능 (Due Date)

LocalDate 필드 추가

getStatus()로 마감 상태 표시 (D-2, D-day, OVERDUE)

Gson LocalDate 처리 추가

삭제 시 기능 추가 및 기능 수정

삭제 전 리스트 확인으로 현재 할 일 확인 기능 추가

비어있을 시 리스트 없음 출력

완료 시 기능 추가 및 기능 수정

완료 전 리스트 확인으로 현재 비완료 항목만 확인

비완료 항목만 표시 기능 추가

1. 📅 마감일 기능 추가 (Due Date)

📌 LocalDate란?

LocalDate는 시간 정보 없이 "연-월-일"만 저장하는 클래스

✅ 생성 관련 메서드

메서드설명예시
LocalDate.now()현재 날짜 가져오기LocalDate today = LocalDate.now();
LocalDate.of(2025, 6, 14)특정 날짜 생성LocalDate date = LocalDate.of(2025, 6, 14);
LocalDate.parse("2025-06-14")문자열 날짜 파싱LocalDate date = LocalDate.parse("2025-06-14");

⏳ 날짜 비교 메서드

메서드설명예시
isBefore(date)인자로 준 날짜보다 앞인가?a.isBefore(b) → a < b
isAfter(date)인자로 준 날짜보다 뒤인가?a.isAfter(b) → a > b
isEqual(date)날짜가 같은가?a.isEqual(b)

📏 날짜 차이 계산

메서드설명예시
a.until(b).getDays()a에서 b까지 며칠 차이LocalDate.now().until(deadline).getDays();
ChronoUnit.DAYS.between(a, b)더 명시적으로 차이 구함ChronoUnit.DAYS.between(a, b) (import 필요)

🔧 Todo 객체에 마감일 추가

private LocalDate dueDate;

public Todo(String task, boolean isDone, LocalDate dueDate) {
    this.task = task;
    this.isDone = isDone;
    this.dueDate = dueDate;
}

📌 할 일 상태 추가 메서드

public String getStatus() {
    if (dueDate == null) return "";
    LocalDate today = LocalDate.now();
    if (dueDate.isBefore(today)) return "OVERDUE";
    else if (dueDate.isEqual(today)) return "D-DAY";
    else return "D-" + today.until(dueDate).getDays();
}

🧪 테스트 중 예외 발생

NullPointerException: Cannot invoke "java.util.List.add(Object)" because "this.todos" is null

todos가 null이라서 .add() 호출 시 예외 발생

loadTodosFromFile()에서 null 반환 가능성 존재

해결:

return (todos != null) ? todos : new ArrayList<>();

💥 LocalDate Gson 오류 (Java 9+)

InaccessibleObjectException: java.time.LocalDate.year 접근 불가
Java 9 이상에서는 LocalDate의 내부 필드에 리플렉션 접근 제한

LocalDateAdapter 클래스 생성

public class LocalDateAdapter implements JsonSerializer<LocalDate>, JsonDeserializer<LocalDate> {
    private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE;

    @Override
    public JsonElement serialize(LocalDate date, Type type, JsonSerializationContext context) {
        return new JsonPrimitive(date.format(formatter));
    }

    @Override
    public LocalDate deserialize(JsonElement json, Type type, JsonDeserializationContext context)
            throws JsonParseException {
        return LocalDate.parse(json.getAsString(), formatter);
    }
}

!

🧪 테스트 중에 할 일을 추가했는데 에러 발생

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.List.add(Object)" because "this.todos" is null
	at TodoService.add(TodoService.java:12)
	at Main.main(Main.java:17)

NullPointerException
발생
현재 JSON의 데이터가 이상해서
처음에 어레이 리스트를 로드할 때

ArrayList<Todo> todos = gson.fromJson(reader, type);
System.out.println("✅ 불러오기 완료");
return todos;

여기서 todos → null이 되어버림
null이 되어버리면 add() 불가
add() 시 NPE 예외 발생해버림
null로 처리됨

그래서

return (todos != null) ? todos : new ArrayList<>();

로 해결 → null 발생 시 리스트 초기화 ✅

### ⚠️ TestSaveAndLoad Tool 오류 발생


Caused by: java.lang.reflect.InaccessibleObjectException: 
Unable to make field private final int java.time.LocalDate.year accessible: 
module java.base does not "opens java.time" to unnamed modul

Java 9 이상에서는 java.time.LocalDate 클래스는 모듈 시스템으로 보호되어 있어서,
Gson이 리플렉션(reflection) 으로 내부 필드(year, month, day)에 접근 못해서 터짐

쉽게 말해서 Gson이 LocalDate를 저장하지 못해서,
날짜는 이렇게 바꿔 달라는 클래스 생성 필요

import com.google.gson.*;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class LocalDateAdapter implements JsonSerializer<LocalDate>, JsonDeserializer<LocalDate> {
    private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE;

    @Override
    public JsonElement serialize(LocalDate date, Type type, JsonSerializationContext context) {
        return new JsonPrimitive(date.format(formatter)); // "2025-06-14" 같은 문자열로 저장
    }

    @Override
    public LocalDate deserialize(JsonElement json, Type type, JsonDeserializationContext context)
            throws JsonParseException {
        return LocalDate.parse(json.getAsString(), formatter);
    }
}

💾 후 TodoManager 리팩토링

public class TodoManager {

    private static Gson buildGson() {
        return new GsonBuilder()
            .registerTypeAdapter(LocalDate.class, new LocalDateAdapter())
            .setPrettyPrinting()
            .create();
    }

    public static void saveTodosToFile(List<Todo> todos, String filename) {
        try (FileWriter writer = new FileWriter(filename)) {
            Gson gson = buildGson();
            gson.toJson(todos, writer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static List<Todo> loadTodosFromFile(String filename) {
        try (FileReader reader = new FileReader(filename)) {
            Gson gson = buildGson();
            Type type = new TypeToken<List<Todo>>() {}.getType();
            List<Todo> todos = gson.fromJson(reader, type);
            return (todos != null) ? todos : new ArrayList<>();
        } catch (IOException e) {
            return new ArrayList<>();
        }
    }
}

⚠️ 실행 시 문제 발생

incompatible types: java.util.List<Todo> cannot be converted to java.util.ArrayList<Todo>

📄 Main class

ArrayList<Todo> todos = TodoManager.loadTodosFromFile("todos.json");

TodoManager.loadTodosFromFile()의 반환 타입은 List
그런데 Main.java에서는 ArrayList로 직접 받으려고 해서 타입 불일치 발생

💡 자바에서는 List와 ArrayList는 다르다!

ArrayList는 List의 하위 타입이긴 하지만, 직접 대입은 안 된다
List todos = TodoManager.loadTodosFromFile("todos.json"); 로 디버깅

❓ 메서드 인자를 바꾸면 안 되나?

🚫 OOP 원칙 위배 (구현에 의존하는 구조)

메서드 인자는 가능한 한 "무엇을 할 수 있느냐(List)"에만 관심이 있어야 하고,
"어떻게 구현됐느냐(ArrayList)"는 알 필요 없음
ArrayList에 의존하면, 내부 구조까지 강요하는 셈

📆 마감일 입력 추가

import java.time.format.DateTimeParseException;
TodoService add 메소드 및 Main case1, TodoServiceTest 수정
Statement lambda can be replaced with expression lambda

🛠 TodoService Class add method

public void add(String task, String inputDate) {
    LocalDate dueDate;

    if (inputDate == null || inputDate.trim().isEmpty()) {
        dueDate = LocalDate.now();  // 기본값
    } else {
        try {
            dueDate = LocalDate.parse(inputDate);
        } catch (DateTimeParseException e) {
            System.out.println("⚠️ 마감일 형식이 잘못되었습니다. 오늘 날짜로 설정합니다.");
            dueDate = LocalDate.now();
        }
    }

🖥 Main Class

case 1:
    System.out.print("할 일을 입력하세요: ");
    String task = sc.nextLine();
    System.out.print("마감일을 입력하세요 (yyyy-MM-dd) 또는 Enter: ");
    String input = sc.nextLine();
    service.add(task, input);
    break;

🧾 String으로 할 일과 날짜 받기

add 메소드에서 LocalDate로 파싱 및 잘못 입력되었을 때 및 null 처리 추가

🧪 Test 코드도 LocalDate 추가에 맞게 수정

public void testAddTodo() {
    service.add("복습하기", "2025-12-14");
    assertEquals(1, service.getTodos().size());
    assertEquals("복습하기", service.getTodos().get(0).getTask());
}

@Test
public void testMarkDone() {
    service.add("청소하기", "2025-12-14");
    service.markDone(0);
    assertTrue(service.getTodos().get(0).isDone());
}

@Test
public void testRemoveTodo() {
    service.add("휴식", "2025-12-14");
    service.remove(0);
    assertEquals(0, service.getTodos().size());
}

🗑 2. 삭제 시 기능 추가 및 기능 수정

🔧 개선 내용

삭제 전 리스트 표시 기능 추가

할 일이 없으면 "삭제할 일이 없습니다"라고 출력 후 종료

입력 번호는 사용자 기준(1번부터), 내부에서는 인덱스 변환 처리

📄 TodoService.java

public void remove(int displayIndex) {
    int realIndex = displayIndex - 1;
    if (isValidIndex(realIndex)) {
        todos.remove(realIndex);
        System.out.println("✅ 삭제되었습니다.");
    } else {
        System.out.println("❌ 잘못된 번호입니다.");
    }
}

👉 보이는 인덱스와 실제 인덱스 차이를 int realIndex = displayIndex - 1로 해결

public boolean isEmpty() {
    return todos == null || todos.isEmpty();
}

📌 비어 있거나 null일 시 사용할 메소드 생성

🖥 Main.java

case 4:
    if (service.isEmpty()) {
        System.out.println("❌ 삭제할 일이 없습니다.");
        break; // 아무 일도 안 하고 메뉴로 돌아감
    } else {
        service.printAll();
        System.out.print("삭제할 번호를 입력하세요.");
        int choice3 = sc.nextInt();
        sc.nextLine();
        service.remove(choice3);
        break;
    }

📌 리스트가 비어 있을 땐 "삭제할 일이 없다"

그 외에는 모든 리스트를 보여주고 삭제 진행

✅ 3. 완료 시 기능 추가 및 기능 수정

🔧 개선 내용

완료 전 비완료 항목만 보여줌

할 일이 없으면 안내 후 종료

📄 TodoService.java

public void markDone(int id) {
    int realIndex = id - 1;
    if (isValidIndex(realIndex)) {
        todos.get(realIndex).markDone();
        System.out.println("✅ 완료되었습니다.");
    } else {
        System.out.println("❌ 잘못된 번호입니다.");
    }
}

📝 마찬가지로, 눈에 보이는 것과 실제 인덱스의 차이를 realIndex를 이용해서 해결

public void printIncomplete() {
    boolean hasIncomplete = false;
    for (int i = 0; i < todos.size(); i++) {
        if (!todos.get(i).isDone()) {
            System.out.println((i + 1) + ". " + todos.get(i));
            hasIncomplete = true;
        }
    }
    if (!hasIncomplete) {
        System.out.println("✅ 완료할 일이 없습니다.");
    }
}

🔍 isDone이 false인 즉, 완료 안 된 할 일만 표출되는 메소드 생성

📌 완료 안 된 할 일이 없을 경우 "완료할 일이 없습니다." 출력

📄 Main.java

case 3:
    if (service.isEmpty()) {
        System.out.println("❌ 할 일이 없습니다.");
    } else {
        service.printIncomplete(); 
        System.out.print("완료할 번호를 입력하세요: ");
        int choice2 = sc.nextInt();
        sc.nextLine();
        service.markDone(choice2);
    }
    break;

📌 리스트가 비었거나 null일 시 "할 일이 없습니다." 출력

그 외에는 비완료 항목만 출력

🧠 리뷰

기능을 추가하는 게 이렇게 어려운 일인 줄 몰랐다.
원래 짜여 있는 코드를 다 건드려야 하고 테스트로 만들었던 코드마저 건드려야 해서
오류가 오류를 만들고 또 만들어서 머리가 터질 뻔했다.

갑자기 리플렉션? 상향화? 그게 뭔데… 너무 어려운 말들로 날 힘들게 해…
그리고 기능들도 제대로 테스트를 해보니 마음에 안 드는 거 천지여서 이것저것 추가했다.

오류가 오류를 만들고 또 만들어서 머리가 터질 뻔했다.
친구랑 만나서 모각코를 했는데 머리카락 다 빠질 뻔했다는…
뭔가 오늘은 뿌듯보다는 정신이 나가버린…? 😵‍💫

다음엔
🔍 검색 기능
🏷 카테고리 / 태그 기능
📤 CSV 내보내기 기능
↩️ Undo 기능
💾 자동 저장 기능

하려고 했는데…
할 수 있을까…?

profile
초심자

0개의 댓글