빌드노트
Files.write를 한 번 더 호출하니 앞의 기록이 사라졌다
Files.write의 기본 옵션은 기존 파일을 비운다. 실제 내용·크기 비교로 기본 쓰기와 추가 쓰기를 구별하고, CREATE + APPEND와 CREATE_NEW의 실패 조건에 맞춰 호출부를 고른다.
Files.write(path, bytes)를 반복했는데 마지막 기록만 남는다면 호출부의 쓰기 옵션부터 확인한다. 옵션을 생략하면 기존 일반 파일을 0바이트로 줄인 뒤 새 바이트를 쓴다. 기록을 쌓으려면 추가 쓰기를 명시해야 한다.
누적이 목적이라면 기존 호출을 아래처럼 바꾼다. path는 기록할 파일의 Path, bytes는 이번에 추가할 byte[]다. java.nio.file.Files와 java.nio.file.StandardOpenOption을 import한 Java 메서드 안에서 사용하며, IOException은 호출자에게 전달하거나 기존 오류 처리에서 다룬다.
Files.write(path, bytes,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
APPEND는 끝에 붙이고, CREATE는 파일이 없을 때 만든다. APPEND만 지정하면 없는 파일까지 만들어 주지는 않는다. 이 차이는 최초 기록 때 드러나므로, 기존 파일에 추가하는 경우와 없는 파일을 만드는 경우를 함께 확인해야 한다.
두 번째 기록이 짧아져도 이전 내용은 남지 않았다
Java 21의 Files.write 문서 (새 탭에서 열림)는 옵션 생략을 CREATE, TRUNCATE_EXISTING, WRITE 조합으로 설명한다. 파일이 없으면 만들고, 있으면 비운 뒤 쓴다는 계약이다. 단순히 파일 앞부분만 고치는 동작도 아니다.
이를 확인하려고 새 임시 파일에 UTF-8 바이트 배열 first\n을 쓰고, 같은 경로에 더 짧은 x\n을 옵션 없이 썼다. Windows·NTFS의 OpenJDK 17.0.2에서 각 호출 직후 읽은 결과다. 실제 서비스 장애가 아닌 별도 비교 실험이다.
- 첫 기본 쓰기 뒤: 내용
first\n, 크기 6바이트. - 두 번째 기본 쓰기 뒤: 내용
x\n, 크기 2바이트. - 이어서
next\n을CREATE, APPEND로 쓴 뒤: 내용x\nnext\n, 크기 7바이트.
두 번째 쓰기에서는 첫 기록과 그 뒷부분이 모두 사라졌다. 추가 쓰기로 바꾼 뒤에는 기존 x\n 뒤에 새 기록이 붙었다. 여기서 \n은 입력에 직접 넣은 LF를 표시한 것이며, 바이트 배열을 받는 이 호출이 줄바꿈을 자동으로 붙인 것은 아니다.
자기 코드에서도 같은 경로를 쓰는 호출부를 찾고, 작은 재현 파일에서 쓰기 전후의 Files.size(path)와 실제 내용을 비교한다. UTF-8 텍스트라면 Files.readString(path, java.nio.charset.StandardCharsets.UTF_8)으로 내용을 확인할 수 있다. 큰 기록 파일을 통째로 읽지는 않는다. 옵션 없는 호출인데 결과가 이 비교와 다르면 실제 대상 경로와 다른 쓰기 주체도 확인한다.
없는 경로에 APPEND만 지정한 대조에서는 NoSuchFileException이 발생했고 파일도 생기지 않았다. 같은 경로에 CREATE, APPEND를 지정하자 첫 기록이 만들어졌고 다음 호출은 뒤에 붙었다. 파일 누락을 허용할 것인지에 따라 CREATE를 함께 줄지 결정하면 된다.
덮어쓰기와 신규 전용은 별도로 고른다
누적이 아닌 호출도 의도를 명시할 수 있다. 아래의 path와 bytes는 앞과 같은 대상 경로와 기록할 배열이며, 두 호출은 목적에 따라 선택하는 대안이다.
- 전체 교체:
Files.write(path, bytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE). 옵션 없는 호출과 같은 정책이다. 기존 파일과 없는 경로 모두에서 새 내용만 남는 것을 확인했다. - 새 이름에만 생성:
Files.write(path, bytes, StandardOpenOption.CREATE_NEW). 아직 존재하지 않는 경로를 넘긴다. 이미 있으면 충돌로 처리하고, 덮어쓰기 옵션으로 재시도하지 않는다.
앞의 7바이트 파일에 CREATE_NEW로 blocked\n을 쓰려 하자 FileAlreadyExistsException이 발생했다. 거절 뒤에도 내용은 x\nnext\n, 크기는 7바이트였고 바이트 비교도 같았다. 별도의 없는 경로에서는 최초 생성이 성공하고 두 번째 생성은 거절됐다. 미리 빈 파일을 만들어 둔 경로도 이미 존재하는 파일이므로 CREATE_NEW와 충돌한다.
이 내용 보존은 이름 충돌로 쓰기가 거절된 경우의 관찰이다. 일반 쓰기 도중의 I/O 실패까지 원본 보존을 보장하지는 않는다. Java 17의 Files.write 계약 (새 탭에서 열림)은 파일을 비우거나 일부를 쓴 뒤에도 오류가 날 수 있다고 설명한다. FileAlreadyExistsException이라는 세부 예외형도 선택 사항이므로 다른 제공자에서는 IOException의 원인과 파일 상태를 함께 확인한다.
실측은 Java 17에서 순서대로 쓴 작은 파일에 한정한다. Java 21은 문서만 대조했다. APPEND의 공식 설명 (새 탭에서 열림)은 동시 쓰기의 원자성이 파일시스템에 따라 달라진다고 명시한다. 이 실험은 여러 작성자의 기록 순서·원자성이나 fsync·전원 장애 후 디스크 보존을 검증하지 않았다.