입출력I/O

박민수·2023년 2월 25일
0

자바의 정석

목록 보기
17/17
post-thumbnail

1. 입출력(I/O)이란?

I/O는 Input과 Output의 약자로 입력과 출력을 의미한다.
즉 컴퓨터 내부 또는 외부의 장치와 프로그램간의 데이터를 주고받는 것을 말한다.

입출력의 예를 들면 키보드로 데이터를 입력받거나, System.out.println()을 이용해서 출력하는 것이 있다.

데이터를 주고 받으려면 두 대상을 연결하고 데이터를 전송할 수 있는 무언가가 필요한데 이것을 스트림(stream)이라 한다.(데이터를 다뤘던 스트림과 이름만 같음)

입출력에서 스트림은 데이터를 운반하는데 사용되는 연결통로다.
입력과 출력을 수행하려면 *입력 스트림과 출력스트림이 필요하다.

입력 스트림 : 대상으로부터 자료를 읽어들이는 스트림
출력 스트림 : 대상으로 자료를 출력하는 스트림

스트림은 먼저 보낸 데이터를 먼저 받게 되어 있으며 중간에 건너뜀없이 연속적으로 데이터를 주고받는다.

이는 큐(queue)와 같은 FIFO(First In First Out)구조로 되어 있다고 생각하면 된다.

2. 바이트기반 스트림

InputStreamOutputStream은 모든 바이트기반의 스트림의 조상이며 추상 클래스이다.

이를 입출력 대상에 맞게 구현한 클래스의 종류는 아래와 같다.

InputStreamOutputStream에는 다음과 같은 메서드가 선언되어 있다.

InputStream의 메서드

abstract int read( )
스트림 데이터 1byte를 읽어와서 바이트값으로 반환한다.
반환값은 0~255의 아스키코드값(순수데이터인 실질적인 데이터)이기 때문에 문자로 나타내려면 char로 캐스팅해야한다.
더 이상 읽을 수 없을 때는 -1을 반환한다.

int read(byte b[])
byte[] b 만큼의 데이터를 읽어서 b에 저장하고 읽은 바이트 수를 반환한다.

int read(byte[] b, int off, int len)
최대 len개의 byte를 읽어서 배열 b의 지정된 위치(off)부터 저장한다. 실제로 읽어올 수 있는 데이터가 len개보다 적을 수 있다.

int available( )
읽을 수 있는 바이트 수를 반환한다.
해당 스트림에서 지연없이 읽어 들일 수 있는 바이트의 수를 반환한다.

long skip(long n)
InputStream에서 n 바이트 만큼 데이터를 건너뛰고 바이트 수를 반환한다.

void close( )
입력 소스를 닫음으로써 사용하고 있던 자원을 반환한다.

void mark(int readlimit)
현재 위치를 표시한다.
후에 reset()에 의해서 표시해 놓은 위치로 다시 돌아갈 수 있다.
readlimit은 되돌아 갈 수 있는 byte의 수를 의미한다.

boolean markSupported()
해당 InputStream에서 mark()로 지정된 지점이 있는지 여부를 체크한다.

void reset()
mark() 를 마지막으로 호출한 위치로 이동한다.
OutputStream의 메서드

abstract void write(int b)
주어진 값을 출력소스에 쓴다.

void write(byte[] b) 
주어진 배열 b에 저장된 모든 내용을 출력소스에 쓴다.

void write(byte[] b , int off, int len)
주어진 배열 b에 저장된 내용 중 off번째부터 len 만큼을 읽어서 출력소스에 쓴다.

void close( )
입력 소스를 닫음으로써 사용하고 있던 자원을 반환한다.

void flush() 
스트림의 버퍼에 있는 모든 내용을 출력소스에 쓴다.

flush()는 버퍼가 있는 출력스트림의 경우에만 사용된다.

이때 프로그램이 종료될 때, 사용하고 닫지 않은 스트림을 JVM이 닫아주기는 하지만 꼭 close()를 호출해서 반드시 닫아주는 것이 좋다.

ByteArrayInputStream과 같이 메모리를 사용하는 스트림과 System.in, System.out과 같은 표준 입츌력 스트림은 닫아주지 않아도 된다.

왜냐하면 바이트배열은 사용하는 자원이 메모리 밖에 없으므로 가비지컬렉터에 의해 자동적으로 자원을 반환하기 때문이다.

구현예제

스트림의 종류가 달라도 읽고 쓰는 방법은 동일하다.

예제1

InputStream in = System.in;
OutputStream out = System.out;
            
    int idata = in.read(); // input 은 read 와 연결되어있기 때문에 in.read 를 사욯한다.
		
    out.write(idata); // output 은 write 와 연결되어있기 때문에 out.write 를 사용한다
    out.close(); // output 을 끝내는 매서드

예제2
inSrc를 outSrc로 복사
read()와 write()사용

byte[] inSrc = {0,1,2,3,4,5};
byte[] outSrc = null;

ByteArrayInputStream input = null;
ByteArrayOutputStream output = null;

input = new ByteArrayInputStream(inSrc);
output = new ByteArrayOutputStream();

int data = 0;
while((data = input.read()) != -1){
        output.write(data);
    }

outSrc = output.toByteArray();   
System.out.println(Arrays.toString(inSrc)); //[0, 1, 2, 3, 4, 5]
System.out.println(Arrays.toString(outSrc));//[0, 1, 2, 3, 4, 5]

예제3
read()는 한번에 1 byte만 읽고 쓰므로 배열을 사용해 효율적으로 작업이 이뤄지도록

byte[] inSrc = {0,1,2,3,4,5};
byte[] outSrc = null;
byte[] temp = new byte[6];

ByteArrayInputStream input = null;
ByteArrayOutputStream output = null;

input = new ByteArrayInputStream(inSrc);
output = new ByteArrayOutputStream();

input.read(temp, 0 , temp.length); // 읽어온 데이터를 배열 temp에 담는다.
output.write(temp, 2 , 2); //temp[2]부터 2개의 데이터를 출력

outSrc = output.toByteArray();
System.out.println(Arrays.toString(inSrc)) ; //[0, 1, 2, 3, 4, 5]
System.out.println(Arrays.toString(temp));//[0, 1, 2, 3, 4, 5]
System.out.println(Arrays.toString(outSrc));//[2, 3]

3. 문자기반 스트림

앞서 InputStreamOutputStream은 바이트기반 스트림이기 때문에 단 하나의 값밖에 입력받지 못하고, 따라서 출력도 하나밖에 못한다.

여러개의 값을 출력하기 위해서 문자기반 스트림인 InputStreamReaderOutputStreamWriter을 사용한다.

바이트기반 스트림과 문자기반 스트림은 사용하는 방법은 거의 같다.

바이트기반 스트림의 조상이 InputStreamOutputStream 것처럼 문자기반의 스트림에서는 ReaderWriter가 같은 역할을 한다.

ReaderWriter의 메서드는 byte배열 대신 char배열을 사용한다는 것을 제외하면 이전의 것과 거의 동일하다.

Reader의 메서드

int read()
입력소스로부터 하나의 문자를 읽어온다.
char의 범위인 0~65535범위의 정수를 반환한다.
더 이상 읽을 수 없을 때는 -1을 반환한다.

int read(char[] c)
배열 c 만큼의 데이터를 읽어서 c에 저장한다.

abstract int read(char[] c, int off, int len)
최대 len개의 문자를 읽어서 배열 c의 지정된 위치(off)부터 읽은만큼 저장한다.
읽어 온 데이터의 개수 또는 -1을 반환한다.

long skip(long n)
현재 위치에서 주어진 문자 수(n)만큼을 건너뛴다.

void close( )
입력 스트림을 닫음으로써 사용하고 있던 자원을 반환한다.

void mark(int readlimit)
현재 위치를 표시한다.
후에 reset()에 의해서 표시해 놓은 위치로 다시 돌아갈 수 있다.
readlimit은 되돌아 갈 수 있는 byte의 수를 의미한다.

boolean markSupported()
mark()와 reset()을 지원하는지를 알려준다.

void reset()
입력소스에서의 위치를 마지막으로 mark()가 호출되었던 위치로 되돌린다.
Writer의 메서드

Writer append(char c)
지정된 문자를 출력소스에 출력한다.

Writer append(CharSequence c)
지정된 문자열을 출력소스에 출력한다.

Writer append(CharSequence c, int start, int end)
지정된 문자열의 일부를 출력소스에 출력

void write(int b)
주어진 값을 출력소스에 쓴다.

void write(char[] c) 
주어진 배열 c에 저장된 모든 내용을 출력소스에 쓴다.

abstract void write(char[] c , int off, int len)
주어진 배열 c에 저장된 내용 중 off번째부터 len만큼을 읽어서 출력소스에 쓴다.

void write(String str)
주어진 문자열(str)을 출력소스에 쓴다.

void write(String str, int off, int len)
주어진 문자열(str)의 일부를 출력소스에 쓴다.
(off번째 문자부터 len개 만큼의 문자열)

void close( )
입력 소스를 닫음으로써 사용하고 있던 자원을 반환한다.

void flush() 
스트림의 버퍼에 있는 모든 내용을 출력소스에 쓴다.

구현예제

InputStream in = System.in;
InputStreamReader reader = new InputStreamReader(in); // InputStreamReader 사용하기 위해 객체 생성
		
OutputStream out = System.out;
OutputStreamWriter writer = new OutputStreamWriter(out); // OutputStreamWriter 사용하기 위해 객체 생성
		
char cdata[] = new char[2]; // 이제는 char 를 기본형으로 받을 수 있고, 2개 이상의 값을 배열을 통해 받아올수 있다.
reader.read(cdata);
		
int IcData = cdata[0]-'0'; // 배열이기 때문에 char 로 받은 값을 int로 변환하여 계산하고 싶은 경우 이처럼 사용해야한다.
		
writer.write("입력받은 값 : ");
writer.write(cdata);
writer.write("\n");
writer.write("입력받은 첫번째 값 + 10 : ");
writer.write(IcData+10+"\n"); // 입력받은 첫번째 값 +10
		
System.out.println("#######결과#######");
writer.close();

하지만 여기서 char[2]처럼 고정으로 설정하면 입력한 값이 몇개든 2개만 출력하게 된다.

때문에 Buffer를 사용하여 효율을 높일 수 있다.

이를 활용한 것이 BufferedReaderBufferedWriter이다.

3. 문자기반의 보조스트림(BufferedReader와 BufferedWriter)

BufferedReader와 BufferedWriter는 버퍼를 이용해서 입출력의 효율을 높일 수 있도록 해준다.

버퍼란?

버퍼는 입력받은 값을 버퍼에 저장해두었다가 버퍼가 가득차거나 개행 문자가 나타나면 버퍼의 내용을 한 번에 전송하게 된다.

컴퓨터에 입력받는 값이 많으면 많을 수록 Buffer를 사용하여 데이터를 입출력 하는게 Scanner를 통해 하나하나 출력하는 것보다 훨씬 빠르다.

BufferedReader의 자주 사용하는 메서드

readLine() : Reads a line of text.
String 타입으로 한 줄을 읽어온다

BufferedWriter의 자주 사용하는 메서드

flush() : Flushes the stream.
버퍼에 남은 값 출력 && 버퍼 초기화(비우기)

newLine() : Writes a line separator.
버퍼에서 사용하는 개행 메소드(줄바꿈)

구현예제

예제 1
InputStream in = System.in;	
InputStreamReader reader = new InputStreamReader(in);
		
OutputStream out = System.out;
OutputStreamWriter writer = new OutputStreamWriter(out);

// 위의 4줄이 아래의 하나의 줄로 줄어든다.
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
 }

예제2
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

String s = br.readLine(); // bufferedwriter 의 기본형은 String
		
int i = Integer.parseInt(s) +10; // String 을 int로 형변환 후 +10 !!
		
br.close(); // bufferedreader 도 입력을 마쳤다면 닫아주자
		
bw.write("입력받은 값 : "+ s); // 출력
bw.newLine(); // 개행 메소드
bw.write("입력받은 값 +10 : "+i+"\n"); // 이렇게 하니까 제대로 출력됨

bw.flush(); // 남은 값 출력 && 버퍼 초기화
bw.close(); // bufferedwriter 닫기

Java 입출력(I/O), 스트림(Stream), 버퍼(Buffer) 개념 및 사용법

4. File

파일은 기본적이면서도 가장 많이 사용하는 입출력 대상이다.

자바에서는 File클래스를 통해서 파일과 디렉토리를 다룰 수 있도록 한다.

(1) File의 생성자와 경로에 관한 메서드

File(String fileName)
주어진 문자열(fileName)을 이름으로 갖는 파일을 위한 File인스턴스를 생성한다.

File(String pathName, String fileName)
File(File pathName, String fileName)
파일의 경로와 이름을 따로 분리해서 지정할 수 있도록 한 생성자

File(URI uri)
지정된 uri로 파일을 생성

String	getPath()
파일 또는 디렉토리의 *상대 경로 추출 (생성자로 준 경로 반환)
즉 파일의 경로를 String으로 반환한다.

String	getAbsolutePath()
파일 또는 디렉토리의 *절대 경로 추출 (생성자 관계없이 절대 경로 반환)
즉 파일의 절대경로를 String으로 반환한다.

File	getAbsoluteFile()
절대 경로를 가지는 File 객체 생성

String	getParent()
현재 파일의 상위 경로 추출 (생성자에서 제공했을 경우)
즉 파일의 조상 디렉토리를 String으로 반환한다.

File getParentFile()
현재 파일의 상위 경로를 가진 File 객체 생성 (생성자에서 제공했을 경우)
즉 파일의 조상 디렉토리를 File로 반환한다.

구현예제

File f = new File("c:\\jdk1.8\\work\\ch15\\FileEx1.java");
String fileName = f.getName();
int pos = fileName.lastIndexOf(".");

System.out.println("경로를 제외한 파일이름" + fileName);
System.out.println("확장자 제외한 파일이름" + fileName.substring(0, pos));
System.out.println("확장자" + fileName.substring(pos+1));

System.out.println("경로를 포함한 파일이름(상대경로)" + f.getPath());
System.out.println("파일의 절대경로"+ f.getAbsolutePath());
System.out.println("파일이 속해있는 디렉토리"+ f.getParent());

//
경로를 제외한 파일이름FileEx1.java
확장자 제외한 파일이름FileEx1
확장자java
경로를 포함한 파일이름(상대경로)c:\jdk1.8\work\ch15\FileEx1.java
파일의 절대경로c:\jdk1.8\work\ch15\FileEx1.java
파일이 속해있는 디렉토리c:\jdk1.8\work\ch15

절대경로는 최상위 디렉토리부터 해당 파일까지 경유한 모든 경로를 전부 기입하는 방식이다.

상대경로는 현재 파일이 존재하는 디렉토리를 기준으로 해당 파일까지의 위치를 작성한 경로이다.
이동하는 방법은 아래의 것을 이용한다.
./ 현재 위치
../ 현재 기준, 상위 디렉토리 위치

상대경로 예제

c:\jdk1.8\work\ch15\FileEx1 // 현재 경로가 이렇고 내 위치가 `FileEx1`에 있다면 

../는 ch15 디렉토리 공간으로 이동, ../../면 work디렉토리 공간으로 이동이 된다.
(같은 층에 있는 디렉토리 공간으로 이동한다고 생각하면 된다.)

FileEx1에서 ch14/FileEx2 경로를 불러온다면 ../ch14/FileEx2로 불러오면 된다.

(2) File의 메서드

boolean	exists()
파일이 실제 존재하는지 판단

boolean	isDirectory()
디렉토리인지 판단

boolean	isFile()
파일인지 판단

boolean	canRead()
파일이 읽기 가능한지 판단

boolean	canWrite()
파일이 쓰기 가능한지 판단

boolean	canExecute()
파일이 실행 가능한지 판단

boolean	isHidden()
파일이 숨김파일인지 판단

int	length()
파일의 길이(byte) 반환

boolean	renameTo(File dest)	
경로가 같으면 이름 변경, 경로가 다르면 이름 바뀌면서 해당 경로로 이동됨

boolean	delete()
파일 삭제

boolean	mkdir()
생성자에 넣은 경로에 맞게 폴더 생성

boolean	createNewFile()
생성자에 넣은 경로 및 파일명에 맞게 파일 생성

boolean	setReadable(treu/false)
읽기 권한 설정

boolean	setWritable(true/false)
쓰기 권한 설정

boolean	setExecutable(true/false)
실행 권한 설정

String[] list()
현재 경로의 파일 또는 디렉토리 목록 추출

File[] listFiles()
현재 경로의 파일 또는 디렉토리를 가지는 File 타입 배열 추출

구현예제

args에 파일이 정의되있다는 가정하에

public class o {
    static int totalFiles = 0;
    static int totalDirs = 0;
    public static void main(String[] args){

        if(args.length !=1){
            System.out.println("USAGE: java o DIRECTORY");
            System.exit(0);
        }

        File dir = new File(args[0]);

        if(!dir.exists() || !dir.isDirectory()){
            System.out.println("유효하지 않은 디렉토리");
            System.exit(0);
        }

        printFileList(dir);

        System.out.println();
        System.out.println("총" + totalFiles + "개의 파일");
        System.out.println("총" + totalDirs +"개의 디렉토리");
    }

    public static void printFileList(File dir){
        System.out.println(dir.getAbsolutePath()+ "디렉토리");
        File[] files = dir.listFiles();

        ArrayList subDir = new ArrayList<>();

        for(int i = 0; i< files.length; i++){
            String filename = files[i].getName();

            if(files[i].isDirectory()){
                filename = "[" +filename + "]";
                subDir.add(i+"");
                System.out.println(filename);
            }
            int dirNum = subDir.size();
            int fileNum = files.length - dirNum;

            totalFiles += fileNum;
            totalDirs += dirNum;

            System.out.println(fileNum + "개의 파일" + dirNum +"개의 디렉토리");
            System.out.println();

            for(int i =0; i< subDir.size(); i++){
                int index = Integer.parseInt((String)subDir.get(i));
                printFileList(files[index]);
            }
        }
    }
}

파일 메서드는 이정도 있으며, 추후에 파일 업로드와 다운로드 기능을 구현할 예정이다.

profile
쉽게 쉽게

0개의 댓글