정의: 같은 타입의 데이터를 하나로 묶어 저장하는 연속된 공간
String[] todos = new String[10];
✅ Scanner 입력 시 주의할 점
nextInt() 등 숫자를 받을 경우 엔터(개행) 문자가 버퍼에 남아 nextLine()이 자동 스킵되는 문제 발생
해결 방법:
scanner.nextLine(); // 버퍼 초기화 용도
데이터를 설계하는 청사진
public class Todo {
String task; // 할 일
boolean isDone; // 완료 여부
public Todo(String task, boolean isDone) {
this.task = task;
this.isDone = isDone;
}
}
기존 클래스를 물려받아 기능을 그대로 쓰거나 확장하는 방식
public class Car {
String brand = "현대";
public void drive() {
System.out.println("달린다!");
}
}
public class Taxi extends Car {
public void pickUp() {
System.out.println("손님을 태운다");
}
}
public class Main {
public static void main(String[] args) {
Taxi taxi = new Taxi();
taxi.drive(); // Car에서 상속
taxi.pickUp(); // Taxi에서 정의
System.out.println(taxi.brand); // Car에서 상속
}
}
부모 클래스의 메서드를 덮어쓰기(재정의) 할 때 사용
협업 시 가독성과 안전성 ↑
오타 방지, IDE가 자동으로 인식
배열의 업그레이드 버전 (자동으로 길이 늘어남)
import java.util.ArrayList;
ArrayList<Todo> todos = new ArrayList<>();
타입을 고정하지 않고 유연하게 지정할 수 있게 해줌
ArrayList<Todo> todos = new ArrayList<>();
Todo는 제네릭
자바 객체를 JSON 문자열로 변환하거나 그 반대로 변환
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
Gson gson = new Gson();
String json = gson.toJson(todos); // → JSON 문자열로 변환
Type type = new TypeToken<ArrayList<Todo>>(){}.getType();
ArrayList<Todo> loaded = gson.fromJson(json, type); // → 다시 객체로 변환
ArrayList 같은 복잡한 타입은 직접 타입 정보를 알려줘야 Gson이 정확히 처리 가능
FileWriter나 FileReader 사용 시 자동으로 자원 정리
try (FileWriter writer = new FileWriter(filename)) {
writer.write(json);
} catch (IOException e) {
// 예외 처리
}
public class Todo {
public String task; // 할 일
public boolean isDone; // 끝난 일
public Todo(String task, boolean isDone) {
this.task = task;
this.isDone = isDone;
}
@Override
public String toString() {
String checkbox = isDone ? "[x]" : "[]";
return task + "" + checkbox;
}
}
public class Todo {
private String task; // 할 일
private boolean isDone; // 끝난 일
public Todo(String task, boolean isDone) {
this.task = task;
this.isDone = isDone;
}
@Override
public String toString() {
String checkbox = isDone ? "[x]" : "[]";
return task + "" + checkbox;
}
public String getTask() {
return task;
}
public void markDone() {
isDone = true;
}
public boolean isDone() {
return isDone;
}
}
public String task → private String task
public boolean isDone → private boolean isDone
private란?
클래스 내부에서만 접근 가능한 접근 제어자. 외부에서 직접 변경할 수 없게 막는다.
정보 은닉(캡슐화)
→ 객체의 내부 구현은 숨기고, 필요한 기능만 외부에 노출
안정성 확보
→ 데이터가 예기치 않게 변경되지 않도록 보호
isDone = true;를 외부에서 직접 하지 않고,
오직 markDone() 메서드로만 상태를 바꾸도록 제한
읽기 전용 getter 제공
외부에서는 todo.getTask(), todo.isDone()으로 확인만 가능
직접 수정은 ❌ (setter 없음)
직접 필드 접근을 막고, 메서드를 통해 제어된 방식으로만 사용하게 하기 위해서야.
무결성이란?
데이터가 엉망으로 바뀌지 않도록, 일관된 상태를 유지하는 것!
todo.setTask(""); // → 빈 문자열로 변경 (의미 없음)
todo.setTask(null); // → 오류 유발 가능
todo.setIsDone(false); // → 이미 완료한 할 일을 다시 미완료로 바꿈 (혼란)
task는 생성 시 한 번 설정하고 수정 불가
isDone은 오직 markDone()을 통해서만 변경 가능
이렇게 하면 객체의 무결성과 신뢰성을 지킬 수 있다!
import java.util.List;
public class TodoService {
private List<Todo> todos;
public TodoService(List<Todo> todos) {
this.todos = todos;
}
public void add(String task) {
todos.add(new Todo(task, false));
System.out.println("할 일이 추가되었습니다.");
}
public void printAll() {
if (todos.isEmpty()) {
System.out.println("오늘 할 일이 없습니다.");
} else {
for (int i = 0; i < todos.size(); i++) {
System.out.println((i + 1) + "." + todos.get(i));
}
}
}
public void markDone(int id) {
if (isValidIndex(id)) {
todos.get(id).markDone();
System.out.println("✅ 완료되었습니다.");
} else {
System.out.println("❌ 잘못된 번호입니다.");
}
}
public void remove(int id) {
if (isValidIndex(id)) {
todos.remove(id);
System.out.println("삭제되었습니다.");
} else {
System.out.println("❌ 잘못된 번호입니다.");
}
}
public List<Todo> getTodos() {
return todos;
}
private boolean isValidIndex(int index) {
return index >= 0 && index < todos.size();
}
}
.remove()는 ArrayList의 메서드고,
markDone()은 Todo 클래스의 메서드인데...
왜 TodoService 안에서 이걸 다 쓸 수 있는 걸까?
인터페이스 타입(List)으로 선언하고, 다양한 구현체(ArrayList, LinkedList)를 넣어서 쓸 수 있는 것!
코드의 유연성과 확장성을 높이기 위한 설계 패턴이야.
예시:
List<Todo> todos = new ArrayList<>();
// → 추후에 LinkedList로도 바꾸기 쉬움
그래서 .remove() 같은 ArrayList 메서드를 쓸 수 있는 이유:
List는 인터페이스지만,
실제 객체는 ArrayList → 당연히 ArrayList의 메서드 사용 가능!
→ "Todo라는 객체의 모음"
→ 리스트 안의 각 요소는 Todo 객체
→ 그래서 todos.get(i).markDone() 처럼 Todo의 메서드도 사용 가능
Todo 클래스: 하나의 할 일 객체
TodoService: 여러 개의 Todo를 리스트로 관리
이렇게 역할을 나누고 파일들을 분리하면
→ 재사용성 증가 + 유지보수 편함 + 테스트 쉬움! 💪
private boolean isValidIndex(int index) {
return index >= 0 && index < todos.size();
}
service.markDone(999); // 또는 -1
todos.size()가 5인데, 999는 존재하지 않음
→ IndexOutOfBoundsException 발생
→ 프로그램 꺼짐! ❌
예외 상황을 사전에 방지
사용자에게 "잘못된 번호입니다" 라고 친절히 안내
프로그램이 죽지 않고 안정적으로 작동
if (index >= 0 && index < todos.size()) { ... }
// → 중복 코드
→ isValidIndex(index) 로 통일!
isValidIndex(index)
// → “이 값 유효해?” 라는 의도가 명확!
이번 작업을 통해 객체와 클래스, OOP 메서드,
그리고 인터페이스와 다형성까지
한 방에 정리할 수 있었던 최고의 시간이었다! 😍
import java.util.ArrayList;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
ArrayList<Todo> todos = TodoManager.loadTodosFromFile("todos.json");
TodoService service = new TodoService(todos);
while (true) {
printMenu();
int choice = readInt(sc);
switch (choice) {
case 1:
System.out.print("할 일을 입력하세요: ");
String task = sc.nextLine();
service.add(task);
break;
case 2:
System.out.println("오늘 할 일 목록입니다.");
service.printAll();
break;
case 3:
System.out.println("완료할 번호를 입력하세요.");
int choice2 = sc.nextInt();
sc.nextLine();
service.markDone(choice2);
break;
case 4:
System.out.println("삭제할 번호를 입력하세요.");
int choice3 = sc.nextInt();
sc.nextLine();
service.remove(choice3);
break;
case 5:
TodoManager.saveTodosToFile(todos, "todos.json");
System.out.println("종료합니다.");
return;
default:
System.out.println("잘못된 입력입니다.");
}
}
}
private static void printMenu() {
System.out.println("\n📋 메뉴를 선택해 주세요");
System.out.println("1. 할 일 추가");
System.out.println("2. 현재 할 일 확인");
System.out.println("3. 할 일 완료 처리");
System.out.println("4. 할 일 삭제");
System.out.println("5. 종료");
System.out.print("번호를 입력해 주세요: ");
}
private static int readInt(Scanner sc) {
while (!sc.hasNextInt()) {
System.out.println("숫자를 입력해 주세요!");
sc.next(); // 잘못된 입력 제거
}
int value = sc.nextInt();
sc.nextLine(); // 개행 제거
return value;
}
}
Scanner에서 정수인지 확인하는 함수
정수면 true, 아니면 false 반환
숫자가 아닌 값이 들어오면 경고 후 재입력 받음
📂 Main, TodoService, Todo, TodoManager, Gson까지
OOP 구조를 처음으로 완성한 첫 프로젝트!
너무 멋지고, 의미 있는 첫걸음이었다!
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class SampleTest {
@Test
void testExample() {
assertEquals(2, 1 + 1);
}
}
@Test: 테스트 메서드라는 걸 알려주는 어노테이션
assertEquals(a, b): a와 b가 같은지 확인
import static org.junit.jupiter.api.Assertions.*:
여러 테스트 메서드들을 static import로 한 번에 가져와 사용 (코드 깔끔)
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class TodoTest {
@Test
public void testTodoCreation() {
Todo todo = new Todo("공부하기", false);
assertEquals("공부하기", todo.getTask());
assertFalse(todo.isDone());
}
@Test
public void testMarkDone() {
Todo todo = new Todo("운동하기", false);
todo.markDone();
assertTrue(todo.isDone());
}
}
객체가 생성되었을 때 task와 isDone 값이 잘 들어갔는지 확인 → ✅
markDone() 호출 시 false → true로 잘 바뀌는지 확인 → ✅
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.*;
public class TodoServiceTest {
private TodoService service;
@BeforeEach
public void setUp() {
ArrayList<Todo> todos = new ArrayList<>();
service = new TodoService(todos);
}
@Test
public void testAddTodo() {
service.add("복습하기");
assertEquals(1, service.getTodos().size());
assertEquals("복습하기", service.getTodos().get(0).getTask());
}
@Test
public void testMarkDone() {
service.add("청소하기");
service.markDone(0);
assertTrue(service.getTodos().get(0).isDone());
}
@Test
public void testRemoveTodo() {
service.add("휴식");
service.remove(0);
assertEquals(0, service.getTodos().size());
}
@Test
public void testInvalidIndex() {
service.markDone(10); // 범위를 벗어난 인덱스
service.remove(10);
assertEquals(0, service.getTodos().size()); // 여전히 비어 있어야 함
}
}
각 테스트 실행 전, ArrayList와 TodoService를 초기화
테스트 간 영향 방지 → 독립성 확보!
할 일 추가 시 리스트 크기 및 내용 확인 → ✅
완료 처리(markDone)로 true 변경 확인 → ✅
삭제 후 리스트가 비었는지 확인 → ✅
잘못된 인덱스 접근 시에도 프로그램 안정성 유지 → ✅
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.*;
public class TodoManagerTest {
private final String testFilename = "test_todos.json";
@BeforeEach
public void setUp() {
File file = new File(testFilename);
if (file.exists()) file.delete();
}
@AfterEach
public void tearDown() {
File file = new File(testFilename);
if (file.exists()) file.delete();
}
@Test
public void testSaveAndLoadTodos() {
ArrayList<Todo> todosToSave = new ArrayList<>();
todosToSave.add(new Todo("JUnit 복습", false));
todosToSave.add(new Todo("깃 정리", true));
TodoManager.saveTodosToFile(todosToSave, testFilename);
ArrayList<Todo> loadedTodos = TodoManager.loadTodosFromFile(testFilename);
assertEquals(2, loadedTodos.size());
assertEquals("JUnit 복습", loadedTodos.get(0).getTask());
assertFalse(loadedTodos.get(0).isDone());
assertEquals("깃 정리", loadedTodos.get(1).getTask());
assertTrue(loadedTodos.get(1).isDone());
}
@Test
public void testLoadFromMissingFile() {
ArrayList<Todo> result = TodoManager.loadTodosFromFile("not_exist_file.json");
assertNotNull(result);
assertTrue(result.isEmpty());
}
}
테스트 후, 테스트용 파일 삭제
다음 테스트에 영향 없도록 파일 정리!
파일 존재 여부 확인, 삭제, 생성 등 조작 가능
저장된 todos가 정확히 로드되는지 확인 → ✅
존재하지 않는 파일 로드 시에도 오류 없이 동작 → ✅
💬
공부를 하다 보니 알아야 할 것도 많고, 모르는 것도 많아서 시간이 많이 들게 된다.
이 정도면 하루 안에 끝나겠지 했지만,
막상 해보면 하루 시간이 부족하다. 어제도 그렇고, 오늘도 그렇다.
오늘은 좀 적당히 하자고 다짐했는데...
역시나 양 조절 실패! 😅
그래도 오늘은 리팩토링하면서 객체, 메서드, 클래스 개념을 확실히 잡을 수 있었다.
그리고 테스트 코드라는 걸 처음 써보면서
아, "이런 걸 테스트하는구나" 하고 한 단계 더 성장했다!
백준 문제 먼저 풀고
예외 상황 테스트 더 정리하고
리팩토링 조금 더!
오늘도 멋지게 해낸 나 자신!
🔥 자랑스럽다! 나 멋지다!! 🔥