스트림
1. 스트림
스트림(Stream)은 자바 8에서 도입된 기능으로, 컬렉션이나 배열의 데이터를 반복문 없이 함수형 스타일로 처리할 수 있도록 도와주는 도구입니다. 스트림은 데이터 저장소가 아니라, 데이터를 흘려보내며 가공하는 처리 흐름이며, filter, map, forEach 같은 연산을 통해 조건에 맞는 데이터를 추출하거나 변형할 수 있습니다. 또한 내부 반복 방식을 사용하여 코드가 간결하고 가독성이 좋으며, 병렬 처리도 손쉽게 할 수 있는 장점이 있습니다.
- 데이터를 담는 저장소가 아님
- 원본 데이터를 변경하지 않고 처리 가능
- 연속적이고 선언적인 데이터 처리 가능
- 파이프라인처럼 연결된 연산을 수행함
데이터 → 중간연산 → 중간연산 → ... → 최종연산
List → filter → map → collect
스트림은 데이터를 한 줄씩 처리하며, 중간 연산은 최종 연산이 호출되어야 실행됩니다 (지연 평가)
1. 스트림의 주요 연산
스트림은 중간 연산과 최종 연산으로 나뉩니다.
중간 연산 (결과가 스트림)
- filter(Predicate) : 조건에 맞는 요소만 추출
- map(Function) : 요소 변환
- sorted() : 정렬
- distinct() : 중복 제거
- limit(n), skip(n) : 자르기/건너뛰기
최종 연산 (스트림 종료)
- forEach(Consumer) : 요소 반복 처리
- collect(Collectors) : 결과 수집
- count(), sum(), max(), min() 등
- anyMatch(), allMatch(), noneMatch() 등
import java.util.Arrays;
import java.util.List;
public class StreamExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("apple", "banana", "orange", "apple");
// 1. "a"로 시작하는 과일만 필터링 후 대문자로 변환하고 출력
fruits.stream() // 스트림 생성
.filter(f -> f.startsWith("a")) // 중간 연산1
.map(String::toUpperCase) // 중간 연산2
.distinct() // 중복 제거
.forEach(System.out::println); // 최종 연산
}
}
2. 컬렉션 vs 스트림
2. IO Stream
IO Stream(Input/Output Stream)은 자바에서 데이터를 읽고 쓰는 흐름을 추상화한 개념으로, 파일, 키보드, 네트워크 등 다양한 입출력 장치와의 데이터 전송을 처리할 수 있도록 도와줍니다. 입력 스트림(InputStream)은 외부에서 데이터를 읽어오는 역할, 출력 스트림(OutputStream)은 데이터를 외부로 내보내는 역할을 하며, 기본적으로 바이트 기반 스트림과 문자 기반 스트림(Reader/Writer)으로 나뉩니다. IO 스트림은 계층 구조로 되어 있어, FileInputStream, BufferedReader, PrintWriter 같은 다양한 클래스들이 조합되어 효율적이고 다양한 방식의 입출력을 처리할 수 있도록 설계되어 있습니다.
스트림의 분류
입력 스트림 (Input) | 외부 → 프로그램으로 데이터 입력 | InputStream, Reader |
출력 스트림 (Output) | 프로그램 → 외부로 데이터 출력 | OutputStream, Writer |
바이트 스트림 | 1바이트 단위로 처리 (이미지, 영상 등 이진 데이터) | InputStream, OutputStream |
문자 스트림 | 문자 단위로 처리 (텍스트 데이터) | Reader, Writer |
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamExample {
public static void main(String[] args) {
try (
FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt");
) {
int data;
while ((data = fis.read()) != -1) {
fos.write(data); // 파일 복사
}
System.out.println("파일 복사 완료 (바이트 스트림)");
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class CharStreamExample {
public static void main(String[] args) {
try (
FileReader reader = new FileReader("input.txt");
FileWriter writer = new FileWriter("output.txt");
) {
int ch;
while ((ch = reader.read()) != -1) {
writer.write(ch); // 문자 단위 복사
}
System.out.println("파일 복사 완료 (문자 스트림)");
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. 직렬화
직렬화(Serialization)는 자바에서 객체를 바이트 형태로 변환하여 파일에 저장하거나 네트워크를 통해 전송할 수 있도록 만드는 과정입니다. 이를 통해 프로그램이 종료되더라도 객체의 상태를 저장하거나, 다른 시스템 간에 객체를 전달할 수 있습니다. 직렬화를 위해서는 해당 클래스가 Serializable 인터페이스를 구현해야 하며, 반대로 바이트 데이터를 다시 객체로 복원하는 과정을 역직렬화(Deserialization)라고 합니다. 직렬화된 데이터는 사람이 읽을 수 없는 이진 형식으로 저장되며, 보통 .ser 확장자를 가진 파일에 저장하는 것이 일반적입니다.
- 직렬화할 클래스는 java.io.Serializable 인터페이스를 구현해야 합니다.
- 이 인터페이스는 마커 인터페이스로, 구현할 메서드는 없습니다.
- 클래스의 필드는 모두 직렬화가 가능한 타입이어야 합니다.
- 직렬화되지 말아야 하는 필드는 transient 키워드 사용
import java.io.Serializable;
public class Student implements Serializable {
private static final long serialVersionUID = 1L; // 권장
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return "학생 이름: " + name + ", 나이: " + age;
}
}
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class SerializeExample {
public static void main(String[] args) {
Student s = new Student("김사과", 20);
try (
FileOutputStream fos = new FileOutputStream("student.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
) {
oos.writeObject(s); // 직렬화
System.out.println("객체를 파일에 저장했습니다.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class DeserializeExample {
public static void main(String[] args) {
try (
FileInputStream fis = new FileInputStream("student.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
) {
Student s = (Student) ois.readObject(); // 역직렬화
System.out.println("복원된 객체: " + s);
} catch (Exception e) {
e.printStackTrace();
}
}
}
serialVersionUID
serialVersionUID는 직렬화된 객체의 클래스 버전을 식별하는 고유 ID입니다. 이 ID는 직렬화 당시의 클래스 구조를 구분하는 역할을 합니다.
private static final long serialVersionUID = 1L;
자바는 직렬화된 객체를 읽어올 때, 원래 클래스와 구조가 같아야 역직렬화가 가능합니다. 그런데 클래스 구조가 바뀌면 문제가 생깁니다.
// 저장 당시
class Student implements Serializable {
String name;
}
// 변경 후
class Student implements Serializable {
String name;
int age; // 필드 추가
}
이때 serialVersionUID가 다르면 역직렬화 시 오류가 발생합니다. InvalidClassException 예외가 뜹니다.
// Student.java
import java.io.Serializable;
public class Student implements Serializable {
String name;
int age; // 새 필드 추가
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return "이름: " + name + ", 나이: " + age;
}
}
// 복원 코드
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.ser"));
Student s2 = (Student) ois.readObject(); // ❌ InvalidClassException 발생