
'숫자' - '0' 아스키 코드 연산
Character.getNumericValue()
문자 숫자뿐 아니라 특수 문자도 숫자로 바꿀 수 있음
Integer.parseInt(String)
문자열 전체를 정수로 바꾸는 메소드
문자열이 숫자일 때만 가능
예외 처리를 하려고 try 해보고, 안되면 catch 로 가서 처리
처리 여부와 관계없이 finally 실행하는 구조
예외 처리가 안되면 시스템 자체가 멈춰버리기 때문에
예외가 제대로 처리되는지 확인이 필요함
private boolean isValidIndex(int index) {
return index >= 0 && index < todos.size();
}
이런 함수는 코드 재사용성도 높이고,
가독성, 안정성까지 챙기는 구조적인 코드 스타일
LocalDate 필드 추가
getStatus()로 마감 상태 표시 (D-2, D-day, OVERDUE)
Gson 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 필요) |
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<>();
InaccessibleObjectException: java.time.LocalDate.year 접근 불가
Java 9 이상에서는 LocalDate의 내부 필드에 리플렉션 접근 제한
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);
}
}
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>
ArrayList<Todo> todos = TodoManager.loadTodosFromFile("todos.json");
TodoManager.loadTodosFromFile()의 반환 타입은 List
그런데 Main.java에서는 ArrayList로 직접 받으려고 해서 타입 불일치 발생
ArrayList는 List의 하위 타입이긴 하지만, 직접 대입은 안 된다
List todos = TodoManager.loadTodosFromFile("todos.json"); 로 디버깅
메서드 인자는 가능한 한 "무엇을 할 수 있느냐(List)"에만 관심이 있어야 하고,
"어떻게 구현됐느냐(ArrayList)"는 알 필요 없음
ArrayList에 의존하면, 내부 구조까지 강요하는 셈
import java.time.format.DateTimeParseException;
TodoService add 메소드 및 Main case1, TodoServiceTest 수정
Statement lambda can be replaced with expression lambda
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();
}
}
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;
add 메소드에서 LocalDate로 파싱 및 잘못 입력되었을 때 및 null 처리 추가
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());
}
🔧 개선 내용
삭제 전 리스트 표시 기능 추가
할 일이 없으면 "삭제할 일이 없습니다"라고 출력 후 종료
입력 번호는 사용자 기준(1번부터), 내부에서는 인덱스 변환 처리
public void remove(int displayIndex) {
int realIndex = displayIndex - 1;
if (isValidIndex(realIndex)) {
todos.remove(realIndex);
System.out.println("✅ 삭제되었습니다.");
} else {
System.out.println("❌ 잘못된 번호입니다.");
}
}
public boolean isEmpty() {
return todos == null || todos.isEmpty();
}
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;
}
그 외에는 모든 리스트를 보여주고 삭제 진행
🔧 개선 내용
완료 전 비완료 항목만 보여줌
할 일이 없으면 안내 후 종료
public void markDone(int id) {
int realIndex = id - 1;
if (isValidIndex(realIndex)) {
todos.get(realIndex).markDone();
System.out.println("✅ 완료되었습니다.");
} else {
System.out.println("❌ 잘못된 번호입니다.");
}
}
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("✅ 완료할 일이 없습니다.");
}
}
case 3:
if (service.isEmpty()) {
System.out.println("❌ 할 일이 없습니다.");
} else {
service.printIncomplete();
System.out.print("완료할 번호를 입력하세요: ");
int choice2 = sc.nextInt();
sc.nextLine();
service.markDone(choice2);
}
break;
그 외에는 비완료 항목만 출력
기능을 추가하는 게 이렇게 어려운 일인 줄 몰랐다.
원래 짜여 있는 코드를 다 건드려야 하고 테스트로 만들었던 코드마저 건드려야 해서
오류가 오류를 만들고 또 만들어서 머리가 터질 뻔했다.
갑자기 리플렉션? 상향화? 그게 뭔데… 너무 어려운 말들로 날 힘들게 해…
그리고 기능들도 제대로 테스트를 해보니 마음에 안 드는 거 천지여서 이것저것 추가했다.
오류가 오류를 만들고 또 만들어서 머리가 터질 뻔했다.
친구랑 만나서 모각코를 했는데 머리카락 다 빠질 뻔했다는…
뭔가 오늘은 뿌듯보다는 정신이 나가버린…? 😵💫
다음엔
🔍 검색 기능
🏷 카테고리 / 태그 기능
📤 CSV 내보내기 기능
↩️ Undo 기능
💾 자동 저장 기능
하려고 했는데…
할 수 있을까…?