Java ‐ Input & Output - thought-corner/Backend-PlayGround GitHub Wiki
스트림
- 자바 프로세스가 가지고 있는 데이터를 밖으로 보내려면 출력 스트림을 사용하면 되고 반대로 외부 데이터를 자바 프로세스 안으로 가져오려면 입력 스트림을 사용하면 된다.
- 참고로 각 스트림은 단방향으로 흐른다.
FileOutputStream- 파일에 데이터를 출력하는 스트림
- 파일이 없으면 파일을 자동으로 만들고 데이터를 해당 파일에 저장한다.
- 폴더는 미리 만들어두어야 한다.
write()- byte 단위로 값을 출력한다.
FileInputStream- 파일에서 데이터를 읽어오는 스트림
read()- 파일에서 데이터를 byte 단위로 하나씩 읽어온다.
- 파일의 끝(EOF, End of File)에 도달하면 -1을 리턴한다.
close()- 파일에 접근하는 것은 자바 입장에서 외부 자원을 사용하는 것이다.
- 자바에서 내부 객체는 자동으로 GC가 되지만 외부 자원은 사용 후 반드시 닫아주어야 한다.
부분으로 나누어 읽기와 전체 읽기 비교
read(byte[], offset, length)- 스트림의 내용을 부분적으로 읽거나, 읽은 내용을 처리하면서 스트림을 계속해서 읽어야 할 경우에 적합하다.
- 메모리 사용량을 제어할 수 있다.
- 파일이나 스트림에서 일정한 크기의 데이터를 반복적으로 읽어야 할 때 유용하다.
readAllBytes()- 한 번의 호출로 모든 데이터를 읽을 수 있어 편리하다.
- 작은 파일이나 메모리에 모든 내용을 올려 처리해야 하는 경우에 적합하다.
- 메모리 사용량을 제어할 수 없다.
- 큰 파일의 경우 OutOfMemoryError가 발생할 수 있다.
InputStream/OutputStream
- 현대 컴퓨터는 대부분 byte 단위로 데이터를 주고 받는다. 참고로 bit 단위는 너무 작기 때문에 byte 단위를 기본으로 사용한다.
- 이렇게 데이터를 주고받는 것을 Input/Output(I/O)라 한다.
- 자바 내부에 있는 데이터를 외부에 전달할 때 각각 데이터를 주고 받는 방식이 다르면 불편하기 때문에 이런 문제를 해결하기 위해 자바는
InputStream,OutputStream이라는 기본 추상 클래스를 제공한다.
InputStream과OutputStream이 다양한 스트림을 추상화하고 기본 기능에 대한 표준을 잡아둔 덕에 편리하게 입출력 작업을 수행할 수 있다.- 일관성 : 모든 종류 입출력 작업에 대해 동일한 인터페이스를 사용할 수 있어 코드 일관성이 유지된다.
- 유연성 : 실제 데이터 소스나 목적지가 무엇인지에 관계없이 동일한 방식으로 코드를 작성할 수 있다.
- 확장성 : 새로운 유형의 입출력 스트림을 쉽게 추가할 수 있다.
- 재사용성 : 다양한 스트림 클래스들을 조합해 복잡한 입출력 작업을 수행할 수 있다.
- 에러 처리 : 표준화된 예외 처리 메커니즘을 통해 일관된 방식으로 오류를 처리할 수 있다.
파일 입출력과 성능 최적화
- 자바에서 운영체제를 통해 디스크에 1Byte씩 전달하면 운영체제나 하드웨어 레벨에서 여러가지 최적화가 발생한다.
- 따라서 실제로 디스크에 1Byte씩 계속 쓰는 것은 아니다.
- 자바에서 1Byte씩
write()나read()를 호출할 때마다 운영체제로의 시스템 콜이 발생하고 이 시스템 콜 자체가 상당한 오버헤드를 유발한다. - 운영체제와 하드웨어가 어느 정도 최적화를 제공하더라도 자주 발생하는 시스템 콜로 인한 성능 저하는 피할 수 없다.
- 결국 자바에서
read(),write()호출 횟수를 줄여서 시스템 콜 횟수를 줄여야 한다.
문자 다루기
- 스트림은
byte만 사용할 수 있다. - 스트림을 문자로 저장할 수 있도록 해주는 것이 바로
OutputStreamWriter,InputStreamReader이다.
Reader/Writer
- 모든 데이터는
byte단위로 저장된다. - 따라서 Reader/Writer가 아무리 문자를 다룬다고 해도 문자를 바로 저장할 순 없다.
- 결과적으로 해당 클래스에 문자를 전달하면 결과적으로 내부에서는 지정된 문자 집합을 사용해서 문자를
byte를 인코딩해서 저장한다.
객체 직렬화의 한계
- 객체 직렬화를 사용하지 않는 이유
- 버전 관리의 어려움 - 클래스 구조가 변경되면 이전에 직렬화된 객체와의 호환성 문제가 발생,
serialVersionUID관리가 복잡하다. - 플랫폼 종속성 - 자바 직렬화는 자바 플랫폼에 종속적이어서 다른 언어나 시스템과의 상호 운용성이 떨어진다.
- 성능 이슈 - 직렬화/역직렬화 과정이 상대적으로 느리고 리소스를 많이 사용한다.
- 유연성 부족 - 직렬화된 형식은 커스터마이징하기 어렵다.
- 크기 효율성 - 직렬화된 데이터 크기가 상대적으로 크다.
- 버전 관리의 어려움 - 클래스 구조가 변경되면 이전에 직렬화된 객체와의 호환성 문제가 발생,
결론 : 자바 객체 직렬화는 사용하지 마라.
1. 객체 직렬화의 대안 - XML
- 플랫폼 종속성 문제를 해결하기 위해 XML이라는 기술이 인기를 끌었다.
- 허나 XML은 복잡성과 무거움이라는 문제가 존재한다.
- 태그를 포함한 XML 문서 크기가 커서 네트워크 전송 비용도 증가했다.
2. 객체 직렬화의 대안 - JSON
- JSON은 가볍고 간결하며 자바스크립트와의 자연스러운 호환성 덕분에 웹 개발자들 사이에서 빠르게 확산되었다.
- 웹 API와 RESTful 서비스가 대중화되면서 JSON은 표준 데이터 교환 포맷으로 자리 잡았다.
3. 객체 직렬화의 대안 - Protobuf, Avro
- JSON은 거의 모든 곳에서 호환이 가능하고 사람이 읽고 쓰기 쉬운 포맷이어서 디버깅과 개발이 쉽다.
- 매우 작은 용량으로 더 빠른 속도가 필요하다면 Protobuf, Avro 등을 고려할 수 있다.
- 하지만 이런 기술들은 호환성은 떨어지지만 용량과 성능 최적화가 되어 있어 매우 빠르다.
- 다만 byte 기반이기 때문에 사람이 읽기 쉽지 않다.
1. 보안상의 치명적 약점
- 자바 직렬화의 근본적인 문제는 '보이지 않는 생성자' 역할을 한다는 점이다.
- 역직렬화의 위험성 :
readObject()와 같은 메서드는 바이트 스트림을 읽어 객체를 생성할 때 클래스의 생성자를 호출하지 않는다. 즉, 객체를 생성할 때, 우리가 생성자에 걸어둔 불변성 검사나 보안 체크를 완전히 우회할 수 있게 된다는 것이다.- 가젯과 원격 코드 실행 : 공격자는 정교하게 조작된 바이트 스트림을 보내 시스템 내에 존재하는 특정 클래스 메서드를 연쇄적으로 호출해 서버를 장악할 수 있다. 자바 표준 라이브러리조차도 이런 공격에서 자유롭지 못하다.
2. 유지보수의 족쇄
- 클래스가
Serializable인터페이스를 구현하는 순간, 그 클래스 내부 구현은 외부로 노출되는 공개 API가 되어버린다.serialVersionUID: 클래스에 필드 하나만 추가하거나 이름을 바꿔도 고유 번호가 달라지는데 이를 명시하지 않으면 역직렬화 시InvalidClassException예외가 발생하며 시스템이 다운된다.- 캡슐화 파괴 : 원래 클래스의
private필드들도 직렬화 데이터에 포함된다. 이는 객체의 내부 구현을 숨겨야 한다는 객체지향 기본 원칙을 정면으로 위배하며, 나중에 내부 구조를 변경하기 극도로 어렵게 만든다.3. 성능 저하 및 용량 문제
- 오버헤드 : 자바 직렬화 데이터에는 객체 타입 정보, 메타데이터 등이 모두 포함된다. JSON이나 Protobuf와 같은 포맷에 비해 바이트 크기가 훨씬 크고 전송 속도와 저장 공간 면에서 매우 비효율적이다.
- 그래프 순회 비용 : 복잡하게 얽힌 객체 그래프를 직렬화할 때, 자바는 이 연결된 모든 객체를 찾아다니며 처리하기 때문에 CPU와 메모리를 과도하게 소모한다.