본문 바로가기
Java

[JAVA] java.nio.file을 사용한 간단한 메모장 만들기

by teamnova 2025. 4. 28.
728x90

java.nio.file 패키지를 사용한 모던 자바 파일 입출력(I/O) 방법과 사용자 입력을 처리하는 간단한 메모장을 만들어 보겠습니다.

 

언제 사용하면 좋을까요?

 

1. 설정 파일 또는 데이터 초안 생성: 사용자가 입력한 특정 키 값(첫 줄)을 파일 이름으로 하고 나머지 내용을 값으로 하는 설정 파일

(.txt, .properties 등)의 초안을 만드는 데 활용할 수 있습니다.


2. 사용자 생성 콘텐츠 저장: 사용자가 게시글 제목과 내용을 입력하면, 제목을 기반으로 파일 이름을 만들어 저장하는 간단한 시스템의 로직을 이해하는 데 도움이 됩니다.

 

java.nio.file 장점


1. 직관적인 API: Path 객체로 파일/디렉토리 경로를 명확하게 표현하고, Files 유틸리티 클래스로 다양한 파일 관련 작업을 쉽게 수행할 수 있습니다.


2. 개선된 예외 처리: IOException의 구체적인 하위 클래스들을 제공하여 오류 상황을 더 정확하게 파악하고 처리할 수 있습니다.


3. try-with-resources 지원: Files.newBufferedReader, Files.newBufferedWriter 등 NIO.2 API는 AutoCloseable을 구현하므로, try-with-resources 구문을 사용해 리소스를 자동으로 안전하게 닫을 수 있습니다. 

 

코드

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.stream.IntStream;

public class Main {

    // 내용이 없거나 첫 줄이 비었을 때 사용할 기본 파일 이름 (확장자 제외)
    private static final String DEFAULT_BASENAME = "untitled";
    // 파일 확장자
    private static final String FILE_EXTENSION = ".txt";
    // 입력 종료 명령어 정의
    private static final String SAVE_COMMAND = "저장";

    public static void main(String[] args) {
        Path filePath = null; // 파일 경로는 내용 입력 후 결정되므로 null로 초기화
        String finalFilename = ""; // 최종 파일 이름 저장 변수

        try (Scanner scanner = new Scanner(System.in)) {

            // --- 1 단계: 사용자로부터 초기 내용 입력받기 ---
            System.out.println("파일에 저장할 초기 내용을 입력하세요.");
            System.out.println("첫 번째 줄 내용이 파일 이름의 기반이 됩니다.");
            System.out.println("다 입력했으면 새 줄에 '" + SAVE_COMMAND + "' 라고 입력하세요.");
            List<String> initialContent = getContentFromUser(scanner);

            // --- 2 & 3 & 4 단계: 파일 이름 결정 ---
            String baseName;
            if (initialContent.isEmpty() || initialContent.get(0).trim().isEmpty()) {
                // 내용이 없거나 첫 줄이 비어있으면 기본 이름 사용
                baseName = DEFAULT_BASENAME;
                System.out.println("[알림] 첫 줄이 비어있어 기본 파일 이름 '" + baseName + FILE_EXTENSION + "'을(를) 사용합니다.");
            } else {
                // 첫 줄 내용을 파일 이름으로 사용하기 위해 가공
                baseName = sanitizeFilename(initialContent.get(0));
            }
            finalFilename = baseName + FILE_EXTENSION;
            filePath = Paths.get(finalFilename); // 최종 Path 객체 생성

            // --- 5 단계: 파일 생성 및 초기 내용 쓰기 ---
            if (writeFileContent(filePath, initialContent)) {
                System.out.println("\n[성공] 파일 '" + finalFilename + "'이(가) 생성되고 초기 내용이 저장되었습니다.");
                System.out.println("파일 위치: " + filePath.toAbsolutePath());

                // --- 생성된 파일 내용 확인 ---
                System.out.println("\n--- 생성된 '" + finalFilename + "' 파일 내용 확인 ---");
                readFileAndDisplay(filePath);
                System.out.println("\n 생성된 파일을 수정하시겠습니까?");
                System.out.println("\n 1.예 / 2.아니오");
                int str = scanner.nextInt();
                if (str == 1) {
                    // 생성된 파일을 수정 할 때
                    // --- 6 단계: 사용자로부터 수정된 내용 입력받기 ---
                    System.out.println("\n--- 파일 '" + finalFilename + "' 내용 수정 ---");
                    System.out.println("아래 내용을 참고하여 수정된 전체 내용을 다시 입력하세요.");
                    System.out.println("마찬가지로 다 입력했으면 새 줄에 '" + SAVE_COMMAND + "' 라고 입력하세요.");
                    List<String> modifiedContent = getContentFromUser(scanner);

                    // --- 7 단계: 수정된 내용으로 파일 덮어쓰기 (동일한 파일 이름 사용) ---
                    if (writeFileContent(filePath, modifiedContent)) {
                        System.out.println("\n[성공] 파일 '" + finalFilename + "'이(가) 수정된 내용으로 덮어쓰기 되었습니다.");

                        // --- 8 단계: 최종 수정된 파일 내용 확인 ---
                        System.out.println("\n--- 최종 수정된 '" + finalFilename + "' 파일 내용 확인 ---");
                        readFileAndDisplay(filePath);

                    } else {
                        System.out.println("\n[실패] 파일 수정 내용 저장에 실패했습니다.");
                    }
                } else {
                    // 생성된 파일을 수정 하지 않을 때
                    System.out.println("\n 수정을 하지 않습니다.");
                }

            } else {
                System.out.println("\n[실패] 초기 파일 생성에 실패했습니다.");
            }

        } // Scanner는 여기서 자동으로 닫힙니다.
        System.out.println("\n프로그램을 종료합니다.");
    }

    /**
     * 사용자로부터 여러 줄의 입력을 받아 리스트로 반환합니다.
     * 사용자가 SAVE_COMMAND를 입력하면 입력이 종료됩니다.
     *
     * @param scanner 사용자 입력을 받을 Scanner 객체
     * @return 사용자가 입력한 문자열 라인들의 리스트
     */
    private static List<String> getContentFromUser(Scanner scanner) {
        List<String> lines = new ArrayList<>();
        while (true) {
            String line = scanner.nextLine();
            if (line.equalsIgnoreCase(SAVE_COMMAND)) {
                break;
            }
            lines.add(line);
        }
        return lines;
    }

    /**
     * 주어진 경로의 파일을 읽어 각 줄 앞에 번호를 붙여 콘솔에 출력합니다.
     *
     * @param filePath 읽을 파일의 경로
     */
    private static void readFileAndDisplay(Path filePath) {
        // 파일이 실제로 존재하는지 먼저 확인하는 것이 안전합니다.
        if (!Files.exists(filePath)) {
            System.out.println("[오류] 파일 '" + filePath.getFileName() + "'을(를) 찾을 수 없습니다.");
            return;
        }
        try {
            List<String> lines = Files.readAllLines(filePath, StandardCharsets.UTF_8);
            if (lines.isEmpty()) {
                System.out.println("(파일 내용 없음)");
            } else {
                IntStream.range(0, lines.size())
                        .forEach(i -> System.out.println((i + 1) + ": " + lines.get(i)));
            }
        } catch (IOException e) {
            System.err.println("[오류] 파일 '" + filePath.getFileName() + "'을(를) 읽는 중 오류 발생: " + e.getMessage());
        }
    }

    /**
     * 주어진 내용을 파일에 씁니다. 파일이 이미 존재하면 덮어씁니다.
     *
     * @param filePath 쓸 파일의 경로
     * @param content  파일에 쓸 문자열 라인들의 리스트
     * @return 쓰기 성공 여부
     */
    private static boolean writeFileContent(Path filePath, List<String> content) {
        try {
            // 쓰기 전에 상위 디렉토리가 없으면 생성해주는 것이 더 안전할 수 있습니다. (선택 사항)
            // Path parentDir = filePath.getParent();
            // if (parentDir != null && !Files.exists(parentDir)) {
            //     Files.createDirectories(parentDir);
            // }
            Files.write(filePath, content, StandardCharsets.UTF_8);
            return true;
        } catch (IOException e) {
            System.err.println("[오류] 파일 '" + filePath.getFileName() + "'에 쓰는 중 오류 발생: " + e.getMessage());
            return false;
        } catch (Exception e) { // 예상치 못한 다른 예외 처리
            System.err.println("[오류] 파일 '" + filePath.getFileName() + "' 처리 중 예기치 않은 오류 발생: " + e.getMessage());
            return false;
        }
    }

    /**
     * 문자열을 파일 이름으로 사용하기 안전하게 만듭니다.
     * - 앞뒤 공백 제거
     * - 파일 시스템에서 허용하지 않는 문자 제거 (예: \ / : * ? " < > |)
     * - 공백 문자를 밑줄(_)로 변경
     * - 너무 길면 잘라내기 (선택 사항, 여기서는 50자로 제한)
     *
     * @param input 원본 문자열 (주로 파일의 첫 줄)
     * @return 파일 이름으로 사용 가능한 문자열
     */
    private static String sanitizeFilename(String input) {
        // 1. 앞뒤 공백 제거
        String sanitized = input.trim();
        // 2. 파일 이름 금지 문자 제거 (정규 표현식 사용)
        sanitized = sanitized.replaceAll("[\\\\/:*?\"<>|]", ""); // 금지 문자 제거
        // 3. 공백을 밑줄로 변경
        sanitized = sanitized.replaceAll("\\s+", "_"); // 하나 이상의 공백을 밑줄 하나로
        // 4. 너무 길면 자르기 (예: 최대 50자)
        if (sanitized.length() > 50) {
            sanitized = sanitized.substring(0, 50);
        }
        // 5. 혹시 모든 문자가 제거되어 비어있다면 기본 이름 반환
        if (sanitized.isEmpty()) {
            return DEFAULT_BASENAME;
        }
        return sanitized;
    }
}

 

시연 영상