본문 바로가기

멋쟁이사자처럼_부트캠프/Java

[멋쟁이사자처럼 부트캠프 TIL 회고] 백엔드 부트캠프 13기: Java 17일차 Java IO, 버퍼

Java IO(Input/Output)

필요성

1. 데이터의 영속성 제공

  • 프로그램은 메모리에서 실행되므로 프로그램이 종료되면 데이터가 사라짐.
  • 데이터를 파일에 저장하거나 데이터베이스에 기록함으로써 데이터를 영구적으로 보관할 수 있음.

예시:

  • 문서 작성 프로그램에서 작성한 내용을 파일로 저장하고, 나중에 열어서 작업을 이어갈 수 있음.

입출력의 핵심 목적

  1. 데이터 이동: 근원지에서 데이터를 읽어와 목적지로 보냄.
  2. 장치 간 상호작용: 프로그램, 파일, 네트워크, 사용자 입력 등 다양한 장치와 통신.
  3. 효율적인 데이터 처리: 자바 IO API는 버퍼링 등 효율적인 입출력 처리를 지원.

1. 입력(Input)

 

입력의 근원지(Source):

  • 데이터를 읽어오는 출발점입니다.
  • 보통 데이터를 제공하는 장치나 매체를 가리킵니다.

주요 예시:

  • 파일: 텍스트 파일, 이미지 파일 등.
  • 키보드: 사용자의 입력(콘솔).
  • 네트워크: 서버에서 데이터를 가져오는 경우.
  • 데이터베이스: 쿼리 결과를 읽는 경우.

2. 출력(Output)

 

출력의 목적지(Destination):

  • 데이터를 내보내는 도착점입니다.
  • 데이터를 전송받는 장치나 매체를 가리킵니다.

주요 예시:

  • 파일: 새로운 텍스트 파일이나 로그 파일 저장.
  • 콘솔: 화면에 출력.
  • 네트워크: 다른 서버나 클라이언트로 데이터 전송.
  • 데이터베이스: 데이터를 삽입하거나 업데이트.

Java IO는 데이터를 읽고 쓰는 작업을 추상화하여 개발자가 외부 자원과 상호작용할 수 있도록 돕는 도구입니다. 프로그램이 데이터와 상호작용하거나 영속성을 제공하려면 Java IO는 필수적입니다.


Byte 단위 입출력 스트림의 주요 클래스

1. InputStream (입력 스트림)

  • 역할: 외부 자원(파일, 네트워크 등)에서 데이터를 읽어오는 데 사용.
  • 주요 메서드:
    • int read(): 1바이트를 읽어서 반환. 데이터가 없으면 -1 반환.
    • int read(byte[] b): 지정된 바이트 배열 크기만큼 데이터를 읽음.
    • void close(): 스트림을 닫아 리소스를 해제.

2. OutputStream (출력 스트림)

  • 역할: 데이터를 외부 자원으로 내보내는 데 사용.
  • 주요 메서드:
    • void write(int b): 1바이트를 출력.
    • void write(byte[] b): 바이트 배열의 데이터를 출력.
    • void flush(): 스트림에 남아 있는 데이터를 강제로 출력.
    • void close(): 스트림을 닫아 리소스를 해제.

 

주요 하위 클래스

클래스 설명
FileInputStream 파일로부터 데이터를 읽기 위한 스트림.
FileOutputStream 파일에 데이터를 쓰기 위한 스트림.
BufferedInputStream 데이터 읽기 성능을 향상시키는 버퍼링 기능 제공.
BufferedOutputStream 데이터 쓰기 성능을 향상시키는 버퍼링 기능 제공.

 

 

FileInputStream과 FileOutputStream을 사용하여 파일을 복사하는 예제

import java.io.*;

public class ByteStreamExample {
    public static void main(String[] args) {
        String sourceFile = "source.txt"; // 원본 파일
        String targetFile = "target.txt"; // 복사본 파일

        try (FileInputStream fis = new FileInputStream(sourceFile);
             FileOutputStream fos = new FileOutputStream(targetFile)) {

            byte[] buffer = new byte[1024]; // 1KB 버퍼
            int bytesRead;

            // 파일에서 데이터를 읽고 복사본 파일에 쓰기
            while ((bytesRead = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, bytesRead);
            }

            System.out.println("파일 복사가 완료되었습니다!");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

코드 설명

  1. FileInputStream: source.txt 파일에서 데이터를 읽어옴.
  2. FileOutputStream: 읽은 데이터를 target.txt 파일로 씀.
  3. 버퍼 사용 (byte[] buffer):
    • 데이터를 한 번에 1바이트씩 처리하면 느리므로, 1024바이트(1KB) 단위로 읽고 씀.
  4. EOF 확인:
    • read()가 -1을 반환하면 파일 끝(EOF: End Of File)에 도달한 것.
  5. Try-with-resources:
    • 스트림(FileInputStream, FileOutputStream)이 자동으로 닫힘.

입출력 흐름

  1. FileInputStream:
    • 원본 파일의 데이터를 1KB씩 읽음.
  2. FileOutputStream:
    • 읽은 데이터를 복사본 파일에 씀.

실행 결과

원본 파일(source.txt)의 내용이 복사본 파일(target.txt)로 복사됨.

 

Byte 단위 스트림의 특징

  1. 범용성:
    • 텍스트 파일뿐만 아니라 이미지, 오디오, 비디오와 같은 이진 데이터 처리 가능.
  2. 낮은 수준의 작업:
    • 데이터를 바이트 단위로 읽고 쓰므로 고수준 작업에 비해 복잡할 수 있음.
  3. 효율성:
    • 작은 크기의 데이터를 처리할 때 효과적.
  4. 추가 기능 지원:
    • 버퍼링(BufferedInputStream, BufferedOutputStream)을 통해 성능 향상 가능.

Byte 단위 스트림은 저수준 I/O 작업에 적합하며, 데이터 전송 및 파일 복사와 같은 작업에서 자주 사용됨


 

BufferedReader와 InputStreamReader를 함께 사용하는 코드

  • BufferedReader는 InputStreamReader의 기능을 확장하여, 데이터를 한 줄 단위로 효율적으로 읽을 수 있습니다.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

public class BufferedReaderExample {
    public static void main(String[] args) {
        System.out.println("Enter your name:");

        // BufferedReader와 InputStreamReader 사용
        try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
            String name = br.readLine(); // 한 줄을 읽음
            System.out.println("Hello, " + name + "!");
        } catch (IOException e) {
            System.out.println("An error occurred while reading input.");
            e.printStackTrace();
        }
    }
}

실행 과정

  1. 프로그램이 사용자에게 "Enter your name:" 메시지를 출력.
  2. 사용자가 키보드로 문자열 입력 후 엔터를 누름.
  3. BufferedReader는 입력된 한 줄을 readLine()으로 읽어 문자열로 반환.
  4. 읽은 문자열을 출력하여 "Hello, [name]!" 메시지를 화면에 표시.

왜 BufferedReader와 InputStreamReader를 함께 사용하는가?

  1. 효율성: BufferedReader는 버퍼를 사용하여 데이터를 읽기 때문에 성능이 향상됩니다.
  2. 편리성: BufferedReader의 readLine() 메서드는 한 줄씩 데이터를 읽어오는 기능을 제공합니다.
  3. 유연성: InputStreamReader는 다양한 입력 소스를 문자 스트림으로 변환할 수 있어 BufferedReader와 함께 사용할 수 있습니다.

결론

BufferedReader와 InputStreamReader의 조합은 효율적이고 직관적인 방식으로 텍스트 입력을 처리할 수 있도록 도와줍니다.

 


웹 개발에서 자주 사용하는 입출력(I/O) 작업

사용자와 서버 간의 데이터 교환, 파일 처리, 데이터베이스 작업 등

 

1. 네트워크 I/O

  • 역할: 클라이언트-서버 간 데이터를 송수신.

주요 기술:

  • Java Sockets: 네트워크 프로그래밍에서 TCP/UDP 통신을 위해 사용.
// 간단한 소켓 예제
Socket socket = new Socket("www.example.com", 80);
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();

 

  • HTTP 클라이언트: REST API 호출에 사용.
HttpURLConnection connection = (HttpURLConnection) new URL("https://api.example.com").openConnection();
connection.setRequestMethod("GET");
InputStream response = connection.getInputStream();

 

  • Spring RestTemplate / WebClient:
    • RESTful API 호출 시 자주 사용.

2. 파일 입출력

  • 역할: 파일 업로드/다운로드 및 처리.
  • 주요 작업:
    • 파일 업로드: 웹 애플리케이션에서 클라이언트가 서버로 파일을 업로드.
    • 파일 다운로드: 서버가 클라이언트에 파일을 제공.

코드 예제:

  • 파일 읽기:
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
String line;
while ((line = reader.readLine()) != null) {
    System.out.println(line);
}
reader.close();

 

  • 파일 업로드(Spring MVC):
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
    file.transferTo(new File("uploaded/" + file.getOriginalFilename()));
    return "File uploaded successfully!";
}

 


3. JSON, XML 데이터 처리

  • 역할: 클라이언트-서버 간 데이터 교환 형식으로 JSON, XML 사용.
  • 주요 작업:
  • JSON 데이터 읽기/쓰기:
ObjectMapper objectMapper = new ObjectMapper();
MyData data = objectMapper.readValue(jsonString, MyData.class);
String jsonOutput = objectMapper.writeValueAsString(data);
  • XML 데이터 읽기/쓰기:
JAXBContext context = JAXBContext.newInstance(MyData.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
MyData data = (MyData) unmarshaller.unmarshal(new File("data.xml"));

 


4. 데이터베이스 입출력

  • 역할: 데이터를 데이터베이스에서 읽거나 쓰기.
  • 주요 기술:

JDBC(Java Database Connectivity):

Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "user", "password");
PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users");
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
    System.out.println(rs.getString("username"));
}

 

ORM 프레임워크 (Hibernate, JPA):

List<User> users = entityManager.createQuery("SELECT u FROM User u", User.class).getResultList();

 


5. 스트림 입출력 (HTTP 응답/요청)

  • 역할: HTTP 요청 및 응답 스트림 처리.
  • 주요 작업:

Servlet:

  • HTTP 요청/응답 스트림 다루기.
@WebServlet("/stream")
public class StreamServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/plain");
        PrintWriter out = response.getWriter();
        out.println("Hello, world!");
        out.close();
    }
}
 

파일 다운로드:

@GetMapping("/download")
public void downloadFile(HttpServletResponse response) throws IOException {
    File file = new File("example.txt");
    response.setContentType("text/plain");
    response.setHeader("Content-Disposition", "attachment; filename=" + file.getName());
    Files.copy(file.toPath(), response.getOutputStream());
}

6. WebSocket I/O

  • 역할: 실시간 데이터 통신(예: 채팅, 실시간 알림).
  • 코드 예제:
@ServerEndpoint("/websocket")
public class WebSocketServer {
    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        session.getBasicRemote().sendText("Echo: " + message);
    }
}

7. 로그 파일 입출력

  • 역할: 애플리케이션 활동을 기록하여 디버깅 및 모니터링에 활용.
  • 주요 라이브러리:
    • Log4j, SLF4J, Logback:
Logger logger = LoggerFactory.getLogger(MyClass.class);
logger.info("This is a log message");

8. Cache I/O

  • 역할: 자주 사용하는 데이터를 메모리(Cache)에 저장하고 빠르게 읽기.
  • 주요 기술:
    • Redis, Memcached:
      • Spring Data Redis와 같은 라이브러리 사용.
redisTemplate.opsForValue().set("key", "value");
String value = redisTemplate.opsForValue().get("key");

 

이 외에도 RESTful API 데이터 처리, 파일 스트림 변환, 비동기 입출력(NIO, AsynchronousChannel) 등이 자주 사용됩니다.

 


버퍼

버퍼(Buffer)는 데이터 입출력 효율성을 높이고 성능을 최적화하기 위해 사용됩니다.

버퍼란?

  • 버퍼는 데이터를 읽거나 쓸 때 사용하는 임시 저장 공간입니다.
  • 작은 데이터 조각을 한 번에 읽거나 쓰는 대신, 한꺼번에 모아서 처리하는 방식으로 작동합니다.

 

왜 버퍼를 사용하는가?

 

1. 입출력 성능 향상

  • 입출력 작업(특히 파일, 네트워크 등)은 비교적 느리게 처리됩니다.
  • 데이터를 한 바이트씩 읽거나 쓰는 대신, 버퍼를 사용해 데이터를 한 번에 처리하면 I/O 호출 횟수를 줄여 속도가 빨라집니다.

2. 자원의 효율적 사용

  • 디스크나 네트워크와 같은 I/O 작업은 시스템 자원을 많이 소모합니다.
  • 버퍼를 사용하면 I/O 작업 호출 횟수가 줄어들어 CPU, 메모리 등의 자원을 효율적으로 사용할 수 있습니다.

3. 병목 현상 완화

  • 입출력 작업은 CPU보다 느린 경우가 많습니다.
  • 버퍼는 데이터를 임시로 저장함으로써 CPU와 I/O 작업 간 속도 차이를 줄이고 프로그램의 전반적인 성능을 개선합니다.

예시: 버퍼 없는 경우 vs 버퍼 있는 경우

버퍼를 사용하지 않는 코드 (비효율적)

FileInputStream fis = new FileInputStream("example.txt");
int data;
while ((data = fis.read()) != -1) {
    System.out.print((char) data); // 한 바이트씩 읽음
}
fis.close();
  • 한 번에 1바이트씩 읽기 때문에 I/O 호출이 빈번하게 발생.
  • 작은 데이터라면 괜찮지만, 큰 파일에서는 성능이 크게 저하됩니다.

버퍼를 사용하는 코드 (효율적)

BufferedInputStream bis = new BufferedInputStream(new FileInputStream("example.txt"));
int data;
while ((data = bis.read()) != -1) {
    System.out.print((char) data); // 버퍼로 한꺼번에 읽음
}
bis.close();
  • BufferedInputStream이 내부적으로 데이터를 모아 한 번에 읽기 때문에 I/O 호출 횟수가 줄어듦.
  • 성능이 훨씬 더 좋아짐.

버퍼를 사용하는 주요 클래스

  • Java 표준 라이브러리에서 제공하는 버퍼 클래스
    • BufferedInputStream: 바이트 단위 입력 스트림에 버퍼를 추가.
    • BufferedOutputStream: 바이트 단위 출력 스트림에 버퍼를 추가.
    • BufferedReader: 문자 단위 입력 스트림에 버퍼를 추가.
    • BufferedWriter: 문자 단위 출력 스트림에 버퍼를 추가.

버퍼 사용의 장단점

장점:

  1. 입출력 성능이 향상됨.
  2. 시스템 리소스를 효율적으로 사용 가능.
  3. 대량 데이터 처리 시 프로그램의 처리 속도가 빨라짐.

단점:

  1. 버퍼는 메모리를 사용하므로 메모리 사용량이 증가.
  2. 잘못된 크기 설정 시, 효율성이 저하될 수 있음 (너무 크거나 작을 경우).

결론

버퍼는 빠르고 효율적인 데이터 입출력을 위해 사용됩니다. 특히, 파일 처리, 네트워크 스트림, 대량 데이터 작업에서 버퍼의 사용은 성능 차이를 크게 만들어 줍니다. Java에서는 표준 버퍼 클래스들을 제공하므로, 이를 적극 활용해 I/O 작업을 최적화하는 것이 중요합니다.