Java Day4

YDC·2025년 6월 10일

우테코8기

목록 보기
4/23
post-thumbnail

📘 Java Day 3 복습 정리

✅ 배열 (Array)

정의: 같은 타입의 데이터를 하나로 묶어 저장하는 연속된 공간

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;
}

}

✅ 상속 (Inheritance)

기존 클래스를 물려받아 기능을 그대로 쓰거나 확장하는 방식

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에서 상속
    }
}

✅ @Override

부모 클래스의 메서드를 덮어쓰기(재정의) 할 때 사용

협업 시 가독성과 안전성 ↑
오타 방지, IDE가 자동으로 인식

✅ ArrayList

배열의 업그레이드 버전 (자동으로 길이 늘어남)

import java.util.ArrayList;
ArrayList<Todo> todos = new ArrayList<>();

✅ 제네릭(Generic)

타입을 고정하지 않고 유연하게 지정할 수 있게 해줌

ArrayList<Todo> todos = new ArrayList<>();

Todo는 제네릭

✅ Gson (Google JSON 라이브러리)

자바 객체를 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); // → 다시 객체로 변환

✅ TypeToken

ArrayList 같은 복잡한 타입은 직접 타입 정보를 알려줘야 Gson이 정확히 처리 가능

✅ try-with-resources 문법

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;
    }
}

💡 리팩토링 리뷰

🔒 private으로 변경한 이유?

public String task → private String task
public boolean isDone → private boolean isDone

private란?
클래스 내부에서만 접근 가능한 접근 제어자. 외부에서 직접 변경할 수 없게 막는다.

✨ 이점 2가지:

정보 은닉(캡슐화)
→ 객체의 내부 구현은 숨기고, 필요한 기능만 외부에 노출

안정성 확보
→ 데이터가 예기치 않게 변경되지 않도록 보호

✅ markDone() 메서드

isDone = true;를 외부에서 직접 하지 않고,
오직 markDone() 메서드로만 상태를 바꾸도록 제한

✅ getTask()와 isDone() 메서드

읽기 전용 getter 제공

외부에서는 todo.getTask(), todo.isDone()으로 확인만 가능

직접 수정은 ❌ (setter 없음)

❓ 그럼 왜 굳이 메서드로 접근해야 할까?

직접 필드 접근을 막고, 메서드를 통해 제어된 방식으로만 사용하게 하기 위해서야.

🧱 무결성(Integrity)을 지키기 위함

무결성이란?
데이터가 엉망으로 바뀌지 않도록, 일관된 상태를 유지하는 것!

❌ 예시: setter가 있으면 벌어질 수 있는 일

todo.setTask("");      // → 빈 문자열로 변경 (의미 없음)
todo.setTask(null);    // → 오류 유발 가능
todo.setIsDone(false); // → 이미 완료한 할 일을 다시 미완료로 바꿈 (혼란)

🧠 결론

task는 생성 시 한 번 설정하고 수정 불가

isDone은 오직 markDone()을 통해서만 변경 가능

이렇게 하면 객체의 무결성과 신뢰성을 지킬 수 있다!

2️⃣ Main 클래스 기능 TodoService로 분리

📦 TodoService 클래스

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의 메서드 사용 가능!

📦 List의 본질

→ "Todo라는 객체의 모음"
→ 리스트 안의 각 요소는 Todo 객체
→ 그래서 todos.get(i).markDone() 처럼 Todo의 메서드도 사용 가능

🎯 핵심 구분

Todo 클래스: 하나의 할 일 객체

TodoService: 여러 개의 Todo를 리스트로 관리

이렇게 역할을 나누고 파일들을 분리하면
→ 재사용성 증가 + 유지보수 편함 + 테스트 쉬움! 💪

🛡️ isValidIndex() 메서드

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

💥 만약 유효하지 않은 인덱스를 쓰면?

service.markDone(999); // 또는 -1
todos.size()가 5인데, 999는 존재하지 않음

→ IndexOutOfBoundsException 발생

→ 프로그램 꺼짐! ❌

✅ 그래서 isValidIndex()가 필요한 이유:

예외 상황을 사전에 방지

사용자에게 "잘못된 번호입니다" 라고 친절히 안내

프로그램이 죽지 않고 안정적으로 작동

🤔 왜 이걸 함수로 따로 뺐을까?

✨ 이유 1: 중복 제거

if (index >= 0 && index < todos.size()) { ... }

// → 중복 코드

→ isValidIndex(index) 로 통일!

✨ 이유 2: 가독성 향상

isValidIndex(index) 

// → “이 값 유효해?” 라는 의도가 명확!

✨ 진짜 좋고 값진 시간!

이번 작업을 통해 객체와 클래스, OOP 메서드,
그리고 인터페이스와 다형성까지
한 방에 정리할 수 있었던 최고의 시간이었다! 😍

3️⃣ Main 클래스 리팩토링

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;
    }
}

🧪 hasNextInt() 란?

Scanner에서 정수인지 확인하는 함수

정수면 true, 아니면 false 반환

숫자가 아닌 값이 들어오면 경고 후 재입력 받음

🎉 마무리

📂 Main, TodoService, Todo, TodoManager, Gson까지
OOP 구조를 처음으로 완성한 첫 프로젝트!

너무 멋지고, 의미 있는 첫걸음이었다!

🧪 Java Unit 테스트 도입 + 리팩토링 마무리

✅ Java 테스트 코드 기본 예시

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로 한 번에 가져와 사용 (코드 깔끔)

1️⃣ Todo 클래스 테스트

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로 잘 바뀌는지 확인 → ✅

2️⃣ TodoService 클래스 테스트

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()); // 여전히 비어 있어야 함
    }
}

🔄 @BeforeEach

각 테스트 실행 전, ArrayList와 TodoService를 초기화

테스트 간 영향 방지 → 독립성 확보!

🧪 테스트 요약

할 일 추가 시 리스트 크기 및 내용 확인 → ✅

완료 처리(markDone)로 true 변경 확인 → ✅

삭제 후 리스트가 비었는지 확인 → ✅

잘못된 인덱스 접근 시에도 프로그램 안정성 유지 → ✅

3️⃣ TodoManager 테스트

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());
    }
}

🧹 @AfterEach

테스트 후, 테스트용 파일 삭제

다음 테스트에 영향 없도록 파일 정리!

📁 File 클래스

파일 존재 여부 확인, 삭제, 생성 등 조작 가능

🧪 테스트 요약

저장된 todos가 정확히 로드되는지 확인 → ✅

존재하지 않는 파일 로드 시에도 오류 없이 동작 → ✅

🧠 오늘의 회고

💬
공부를 하다 보니 알아야 할 것도 많고, 모르는 것도 많아서 시간이 많이 들게 된다.
이 정도면 하루 안에 끝나겠지 했지만,
막상 해보면 하루 시간이 부족하다. 어제도 그렇고, 오늘도 그렇다.

오늘은 좀 적당히 하자고 다짐했는데...
역시나 양 조절 실패! 😅
그래도 오늘은 리팩토링하면서 객체, 메서드, 클래스 개념을 확실히 잡을 수 있었다.

그리고 테스트 코드라는 걸 처음 써보면서
아, "이런 걸 테스트하는구나" 하고 한 단계 더 성장했다!

📌 내일은:

백준 문제 먼저 풀고

예외 상황 테스트 더 정리하고

리팩토링 조금 더!

오늘도 멋지게 해낸 나 자신!
🔥 자랑스럽다! 나 멋지다!! 🔥

profile
초심자

0개의 댓글