Java 8 스트림 API, 입출력 처리, XML 처리 및 네트워크 프로그래밍 핵심 기술

1장 Java 8 스트림 라이브러리

Java 8에 도입된 스트림(Stream) API는 데이터 처리를 위한 새로운 접근 방식을 제공합니다. 이 API는 컬렉션, 배열 등 다양한 데이터 구조를 간편하게 처리할 수 있게 해줍니다. Java 8의 스트림 라이브러리는 다음과 같은 주요 인터페이스와 클래스로 구성됩니다:

  1. Stream: 스트림의 핵심 인터페이스로, filter, map, reduce 등 다양한 연산을 제공합니다.
  2. Optional: 값이 존재할 수도 있고 존재하지 않을 수도 있는 컨테이너로, NullPointerException을 방지합니다.
  3. Collector: 스트림 요소를 컬렉션으로 수집하거나 그룹화, 분할 등의 작업을 수행합니다.
  4. Spliterator: 데이터를 여러 부분으로 분할하여 병렬 처리를 가능하게 합니다.
  5. IntStream, LongStream, DoubleStream: 기본 타입을 직접 처리하는 스트림으로 성능을 향상시킵니다.

Java 8의 스트림 라이브러리는 데이터 처리를 더욱 간단하고 효율적으로 만들어주며, 함수형 프로그래밍 패러다임에서 큰 발전을 이루었습니다.

1.1 반복에서 스트림으로

Java에서 데이터 처리는 전통적으로 for-each 루프를 사용해왔지만, Java 8에서는 스트림(Stream)을 사용하면 코드를 훨씬 간결하게 작성할 수 있습니다. 다음은 두 가지 방식의 비교입니다:

전통적인 반복 방식:

List<String> 과일목록 = Arrays.asList("사과", "바나나", "오렌지", "배", "포도");
for (String 과일 : 과일목록) {
    if (과일.length() > 5) {
        System.out.println(과일);
    }
}

스트림을 이용한 방식:

List<String> 과일목록 = Arrays.asList("사과", "바나나", "오렌지", "배", "포도");
과일목록.stream().filter(과일 -> 과일.length() > 5).forEach(System.out::println);

스트림을 사용하면 코드가 더 간결하고 명확해집니다. 스트림 연산은 다음과 같은 단계로 이루어집니다:

  1. 스트림 생성: Collection의 stream() 또는 parallelStream() 메소드를 사용합니다.
List<String> 과일목록 = Arrays.asList("사과", "바나나", "오렌지", "배", "포도");
Stream<String> 스트림 = 과일목록.stream();
  1. 중간 연산: filter, distinct, map, sorted, limit, skip 등 다양한 중간 연산을 수행합니다.
Stream<String> 필터된스트림 = 스트림.filter(과일 -> 과일.length() > 5);
  1. 최종 연산: forEach, toArray, reduce, collect 등 최종 결과를 생성하는 연산을 수행합니다.
필터된스트림.forEach(System.out::println);
// 또는 한 줄로 표현
과일목록.stream().filter(과일 -> 과일.length() > 5).forEach(System.out::println);

다음은 두 방식을 모두 보여주는 전체 예제 코드입니다:

import java.util.Arrays;
import java.util.List;

public class 반복vs스트림예제 {
    public static void main(String[] args) {
        // 전통적인 반복 방식
        List<String> 과일목록1 = Arrays.asList("사과", "바나나", "오렌지", "배", "포도");
        for (String 과일 : 과일목록1) {
            if (과일.length() > 5) {
                System.out.println(과일);
            }
        }

        // 스트림을 이용한 방식
        List<String> 과일목록2 = Arrays.asList("사과", "바나나", "오렌지", "배", "포도");
        과일목록2.stream().filter(과일 -> 과일.length() > 5).forEach(System.out::println);
    }
}

출력 결과:

바나나
오렌지
포도
바나나
오렌지
포도

1.2 스트림 생성

Java 8에서는 Stream 클래스를 사용해 다양한 방식으로 스트림을 생성할 수 있습니다:

  1. Collection 인터페이스의 stream() 메소드 사용:
List<String> 목록 = Arrays.asList("사과", "바나나", "오렌지");
Stream<String> 스트림 = 목록.stream();
  1. Arrays 클래스의 stream() 메소드 사용:
String[] 배열 = {"사과", "바나나", "오렌지"};
Stream<String> 스트림 = Arrays.stream(배열);
  1. Stream 클래스의 of() 메소드 사용:
Stream<String> 스트림 = Stream.of("사과", "바나나", "오렌지");
  1. Stream 클래스의 generate() 메소드 사용:
Stream<String> 스트림 = Stream.generate(() -> "안녕");
  1. Stream 클래스의 iterate() 메소드 사용:
Stream<Integer> 스트림 = Stream.iterate(1, n -> n + 1).limit(10);
  1. Files 클래스의 lines() 메소드 사용:
Path 경로 = Paths.get("파일.txt");
try (Stream<String> 스트림 = Files.lines(경로)) {
    // 스트림 처리
} catch (Exception e) {
    e.printStackTrace();
}

다음은 다양한 방식으로 스트림을 생성하는 전체 예제 코드입니다:

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import java.util.function.Supplier;

public class 스트림생성예제 {
    public static void main(String[] args) {
        // Collection을 이용한 스트림 생성
        List<String> 목록 = Arrays.asList("사과", "바나나", "오렌지");
        Stream<String> 스트림1 = 목록.stream();

        // 배열을 이용한 스트림 생성
        String[] 배열 = {"사과", "바나나", "오렌지"};
        Stream<String> 스트림2 = Arrays.stream(배열);

        // Stream.of()를 이용한 스트림 생성
        Stream<String> 스트림3 = Stream.of("사과", "바나나", "오렌지");

        // Stream.generate()를 이용한 스트림 생성
        Supplier<String> 공급자 = () -> "안녕";
        Stream<String> 스트림4 = Stream.generate(공급자);

        // Stream.iterate()를 이용한 스트림 생성
        Stream<Integer> 스트림5 = Stream.iterate(1, n -> n + 1).limit(5);

        // 파일을 이용한 스트림 생성
        Path 경로 = Paths.get("파일.txt");
        try (Stream<String> 스트림6 = Files.lines(경로)) {
            // 스트림 처리
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

1.3 filter, map, flatMap

filter, map, flatMap은 Java Stream API에서 가장 많이 사용되는 연산 메소드입니다:

  1. filter: 주어진 조건을 만족하는 요소만 선택합니다.
List<String> 목록 = Arrays.asList("사과", "바나나", "오렌지", "수박", "복숭아");
Stream<String> 스트림 = 목록.stream();
Stream<String> 새로운스트림 = 스트림.filter(s -> s.length() >= 5);
새로운스트림.forEach(System.out::println); // 수박, 복숭아
  1. map: 각 요소를 다른 형태로 변환합니다.
List<String> 목록 = Arrays.asList("사과", "바나나", "오렌지");
Stream<String> 스트림 = 목록.stream();
Stream<String> 새로운스트림 = 스트림.map(s -> s.toUpperCase());
새로운스트림.forEach(System.out::println); // 사과, 바나나, 오렌지
  1. flatMap: 각 요소를 여러 요소로 변환한 후 하나의 스트림으로 합칩니다.
List<String> 목록 = Arrays.asList("hello", "world");
Stream<String> 스트림 = 목록.stream();
Stream<Character> 새로운스트림 = 스트림.flatMap(s -> s.chars().mapToObj(c -> (char) c));
새로운스트림.forEach(System.out::println); // h, e, l, l, o, w, o, r, l, d

1.4 부분 스트림과 스트림 결합

Java에서는 filter, map, reduce 등을 사용해 부분 스트림을 추출하고 스트림을 결합할 수 있습니다:

부분 스트림 추출:

  1. filter: 조건에 맞는 요소들로 구성된 부분 스트림을 생성합니다.
List<Integer> 숫자목록 = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> 부분목록 = 숫자목록.stream().filter(i -> i > 3).collect(Collectors.toList());
// 결과: [4, 5, 6]
  1. limit: 스트림의 요소 수를 제한합니다.
List<Integer> 숫자목록 = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> 부분목록 = 숫자목록.stream().limit(3).collect(Collectors.toList());
// 결과: [1, 2, 3]
  1. skip: 지정된 수만큼의 요소를 건너뜁니다.
List<Integer> 숫자목록 = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> 부분목록 = 숫자목록.stream().skip(3).collect(Collectors.toList());
// 결과: [4, 5, 6]

스트림 결합:

  1. flatMap: 여러 스트림을 하나로 합칩니다.
List<List<Integer>> 목록들 = Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4), Arrays.asList(5, 6));
List<Integer> 결합목록 = 목록들.stream().flatMap(List::stream).collect(Collectors.toList());
// 결과: [1, 2, 3, 4, 5, 6]
  1. reduce: 스트림의 모든 요소를 하나의 값으로 합칩니다.
List<Integer> 숫자목록 = Arrays.asList(1, 2, 3, 4, 5, 6);
int 합 = 숫자목록.stream().reduce(0, (a, b) -> a + b);
// 결과: 21

1.5 기타 스트림 변환

부분 스트림과 스트림 결합 외에도 Java 스트림은 다양한 변환을 제공합니다:

  1. map: 요소를 다른 형태로 변환합니다.
List<Integer> 숫자목록 = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> 제곱목록 = 숫자목록.stream().map(i -> i * i).collect(Collectors.toList());
System.out.println(제곱목록); // [1, 4, 9, 16, 25, 36]
  1. distinct: 중복 요소를 제거합니다.
List<Integer> 숫자목록 = Arrays.asList(1, 1, 2, 2, 3, 4, 5, 5, 6);
List<Integer> 고유목록 = 숫자목록.stream().distinct().collect(Collectors.toList());
System.out.println(고유목록); // [1, 2, 3, 4, 5, 6]
  1. sorted: 요소를 정렬합니다.
List<Integer> 숫자목록 = Arrays.asList(6, 5, 4, 3, 2, 1);
List<Integer> 정렬목록 = 숫자목록.stream().sorted().collect(Collectors.toList());
System.out.println(정렬목록); // [1, 2, 3, 4, 5, 6]
  1. peek: 각 요소에 대해 작업을 수행하지만 스트림의 요소 자체를 변경하지 않습니다.
List<Integer> 숫자목록 = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> 제곱목록 = 숫자목록.stream()
    .peek(i -> System.out.println("원래 값: " + i))
    .map(i -> i * i)
    .peek(i -> System.out.println("제곱 값: " + i))
    .collect(Collectors.toList());
System.out.println(제곱목록); // [1, 4, 9, 16, 25, 36]

1.6 간단한 축소

Java에서 축소(reduction)는 스트림의 요소들을 하나의 값으로 합치는 작업을 말합니다. Java에는 세 가지 종류의 축소 작업이 있습니다: reduce, collect, summarize.

  1. reduce: 스트림의 요소들을 하나의 값으로 합칩니다.
List<Integer> 숫자목록 = Arrays.asList(1, 2, 3, 4, 5, 6);
int 합 = 숫자목록.stream().reduce(0, (a, b) -> a + b);
System.out.println(합); // 21
  1. collect: 스트림의 요소들을 컬렉션으로 수집합니다.
String[] 배열 = {"hello", "world", "java", "stream"};
List<String> 문자열목록 = Arrays.stream(배열).collect(Collectors.toList());
System.out.println(문자열목록); // [hello, world, java, stream]
  1. summarize: 요소들의 평균, 최대값, 최소값 등을 계산합니다.
List<Integer> 숫자목록 = Arrays.asList(1, 2, 3, 4, 5, 6);
double 평균 = 숫자목록.stream().mapToInt(Integer::intValue).average().getAsDouble();
System.out.println(평균); // 3.5

1.7 Optional 타입

Java 8에서 도입된 Optional은 값이 있을 수도 있고 없을 수도 있는 컨테이너 객체입니다. 주된 목적은 코드에서 null 참조를 사용하는 것을 방지하여 NullPointerException을 피하는 것입니다.

주요 메소드:

  • of(T value): null이 아닌 값을 포함하는 Optional 객체를 생성합니다.
  • empty(): 빈 Optional 객체를 생성합니다.
  • ofNullable(T value): 값이 null이 아니면 해당 값을 포함하는 Optional을, 그렇지 않으면 빈 Optional을 생성합니다.
  • get(): 포함된 값을 반환하며, 값이 없으면 NoSuchElementException을 발생시킵니다.
  • isPresent(): 값이 있는지 확인합니다.
  • orElse(T other): 값이 있으면 해당 값을, 없으면 지정된 기본값을 반환합니다.
  • orElseGet(Supplier<? extends T> other): 값이 있으면 해당 값을, 없으면 공급자가 생성한 값을 반환합니다.
  • orElseThrow(Supplier<? extends X> exceptionSupplier): 값이 있으면 해당 값을, 없으면 지정된 예외를 발생시킵니다.

예제:

import java.util.Optional;

public class Optional예제 {
    public static void main(String[] args) {
        String 문자열 = "hello";

        Optional<String> 옵셔널1 = Optional.ofNullable(문자열);
        System.out.println(옵셔널1.isPresent()); // true
        System.out.println(옵셔널1.get()); // hello

        Optional<String> 옵셔널2 = Optional.empty();
        System.out.println(옵셔널2.isPresent()); // false

        String s1 = 옵셔널1.orElse("world");
        String s2 = 옵셔널2.orElse("world");
        System.out.println(s1); // hello
        System.out.println(s2); // world
    }
}

1.8 결과 수집

Java에서는 collect() 메소드를 사용해 스트림을 컬렉션이나 다른 데이터 구조로 변환할 수 있습니다. Collector는 Collector 인터페이스를 구현하는 함수 객체로, 스트림 요소들을 어떻게 누적할지 정의합니다.

수집 예시:

  1. List로 변환:
List<Integer> 숫자들 = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> 짝수들 = 숫자들.stream()
        .filter(n -> n % 2 == 0)
        .collect(Collectors.toList());
System.out.println(짝수들); // [2, 4]
  1. Set으로 변환:
List<Integer> 숫자들 = Arrays.asList(1, 2, 3, 4, 5);
Set<Integer> 짝수들집합 = 숫자들.stream()
        .filter(n -> n % 2 == 0)
        .collect(Collectors.toSet());
System.out.println(짝수들집합); // [2, 4]
  1. Map으로 변환:
List<String> 이름들 = Arrays.asList("John", "Jane", "Adam", "Tom");
Map<String, Integer> 이름길이맵 = 이름들.stream()
        .collect(Collectors.toMap(
                이름 -> 이름,
                이름 -> 이름.length()
        ));
System.out.println(이름길이맵); // {John=4, Jane=4, Adam=4, Tom=3}

1.9 맵으로 수집

Collectors.toMap() 메소드를 사용해 요소들을 맵으로 수집할 수 있습니다. 다음은 Person 클래스를 예로 들어 설명합니다:

Person 클래스:

public class Person {
    private int id;
    private String 이름;

    public Person(int id, String 이름) {
        this.id = id;
        this.이름 = 이름;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return 이름;
    }
}

Person 객체를 맵으로 수집:

List<Person> 사람들 = Arrays.asList(
        new Person(1, "John"),
        new Person(2, "Joe"),
        new Person(3, "Mary")
);

Map<Integer, String> 맵 = 사람들.stream()
        .collect(Collectors.toMap(Person::getId, Person::getName));

System.out.println(맵); // {1=John, 2=Joe, 3=Mary}

키가 중복될 경우 병합 함수를 사용할 수 있습니다:

List<Person> 사람들 = Arrays.asList(
        new Person(1, "John"),
        new Person(2, "Joe"),
        new Person(3, "Mary"),
        new Person(4, "Joe")
);

Map<Integer, String> 맵 = 사람들.stream()
        .collect(Collectors.toMap(
                Person::getId, 
                Person::getName, 
                (이름1, 이름2) -> 이름1 + ", " + 이름2
        ));

System.out.println(맵); // {1=John, 2=Joe, 3=Mary, 4=Joe}

1.10 그룹화와 분할

Java 8 스트림 라이브러리는 요소들을 지정된 조건에 따라 그룹화하거나 분할하는 기능을 제공합니다:

그룹화:

List<Person> 사람들 = Arrays.asList(
        new Person(1, "John"),
        new Person(2, "Joe"),
        new Person(3, "Mary"),
        new Person(4, "Joe")
);

Map<String, List<Person>> 그룹 = 사람들.stream().collect(Collectors.groupingBy(Person::getName));

System.out.println(그룹);
// 출력: {John=[Person@58ceff1], Joe=[Person@7ef20235, Person@4f023edb], Mary=[Person@56cbfb61]}

분할: 분할은 특수한 종류의 그룹화로, 요소들을 true와 false 두 그룹으로 나눕니다:

List<Integer> 숫자들 = Arrays.asList(1, 2, 3, 4, 5);
Map<Boolean, List<Integer>> 분할된맵 = 숫자들.stream().collect(Collectors.partitioningBy(n -> n % 2 == 0));

System.out.println(분할된맵); // {false=[1, 3, 5], true=[2, 4]}

1.11 하위 수집기

Java 8의 하위 수집기(downstream collector)는 그룹화나 분할 작업 시 중첩된 수집 작업을 수행할 수 있게 해줍니다:

그룹화와 하위 수집기:

List<Person> 사람들 = Arrays.asList(
        new Person(1, "John", 25),
        new Person(2, "Joe", 30),
        new Person(3, "Mary", 27),
        new Person(4, "Joe", 35),
        new Person(5, "Mary", 28)
);

Map<String, Double> 평균나이 = 사람들.stream()
        .collect(Collectors.groupingBy(Person::getName, Collectors.averagingInt(Person::getAge)));

System.out.println(평균나이); // {Mary=27.5, Joe=32.5, John=25.0}

분할과 하위 수집기:

List<Integer> 숫자들 = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Map<Boolean, List<Integer>> 분할된숫자들 = 숫자들.stream()
        .collect(Collectors.partitioningBy(n -> n % 2 == 0));

List<Integer> 짝수들 = 분할된숫자들.get(true);
List<Integer> 홀수들 = 분할된숫자들.get(false);

System.out.println("짝수: " + 짝수들); // [2, 4, 6, 8, 10]
System.out.println("홀수: " + 홀수들); // [1, 3, 5, 7, 9]

1.12 기본 타입 스트림

Java 8 이전에는 Java의 컬렉션 프레임워크가 객체 타입만 지원했습니다. 기본 타입(int, double 등)을 사용하려면 해당하는 래퍼 타입(Integer, Double 등)으로 변환해야 했습니다. 이는 성능 문제와 메모리 사용량 증가를 초래할 수 있었습니다.

Java 8에서는 이 문제를 해결하기 위해 기본 타입 스트림(IntStream, LongStream, DoubleStream)을 도입했습니다. 이 스트림들은 기본 타입을 직접 처리하므로 자동 박싱/언박싱의 필요성을 없애고 성능을 향상시킵니다.

기본 타입 스트림 예시:

int[] 숫자들 = {1, 2, 3, 4, 5};
int 합 = IntStream.of(숫자들).sum();
System.out.println(합); // 15

1.13 병렬 스트림

Java 8에서는 스트림의 병렬 처리 개념을 도입하여 여러 스레드에서 동시에 스트림 요소를 처리할 수 있게 했습니다. 이는 대규모 데이터 세트 처리 시 속도와 효율성을 향상시킬 수 있습니다.

병렬 스트림 예시:

int[] 숫자들 = {1, 2, 3, 4, 5};
int 합 = Arrays.stream(숫자들)
               .parallel()
               .sum();
System.out.println(합); // 15

병렬 스트림은 parallel() 메소드를 통해 활성화할 수 있습니다. 하지만 병렬 처리가 항상 더 빠른 것은 아니며, 데이터 세트의 크기와 계산의 복잡성에 따라 달라집니다. 따라서 병렬 스트림을 사용하기 전에 데이터 세트의 크기와 계산의 복잡성을 평가하고, 성능이 향상될 것이라고 확신할 때만 사용해야 합니다.

2장 입출력 처리

Java의 입출력(I/O)은 데이터를 읽고 쓰는 방식을 처리합니다. Java에서 표준 입출력은 System 클래스를 통해 구현됩니다. 다음은 Java에서 일반적으로 사용되는 입출력 방식입니다:

  1. 콘솔 입출력: System.in과 System.out을 사용해 콘솔에서 데이터를 읽고 콘솔에 출력합니다.
  2. 파일 입출력: FileInputStream, FileOutputStream, BufferedReader, BufferedWriter 등을 사용해 파일을 읽고 쓸 수 있습니다.

2.1 입출력 스트림

Java의 입출력 스트림(I/O Stream)은 데이터를 읽고 쓰는 메커니즘입니다. Java에서는 다양한 클래스와 인터페이스로 구성된 계층 구조를 제공하며, 주요 추상 클래스는 InputStream, OutputStream, Reader, Writer입니다.

Java의 입출력 스트림은 크게 두 가지 유형으로 나뉩니다:

  1. 바이트 스트림: 바이트 단위로 데이터를 읽고 씁니다. 이진 데이터(이미지, 오디오, 비디오 등) 처리나 네트워크 데이터 전송에 적합합니다.
  2. 문자 스트림: 문자 단위로 데이터를 읽고 씁니다. 텍스트 데이터 처리에 적합합니다.

바이트 스트림 예시:

// 파일에서 바이트 읽기
try (FileInputStream fis = new FileInputStream("입력.txt")) {
    byte[] 버퍼 = new byte[1024];
    int 길이;
    while ((길이 = fis.read(버퍼)) != -1) {
        System.out.write(버퍼, 0, 길이);
    }
} catch (IOException e) {
    e.printStackTrace();
}

// 파일에 바이트 쓰기
try (FileOutputStream fos = new FileOutputStream("출력.txt")) {
    String 문자열 = "Hello World!";
    byte[] 버퍼 = 문자열.getBytes();
    fos.write(버퍼);
} catch (IOException e) {
    e.printStackTrace();
}

문자 스트림 예시:

// 파일에서 문자 읽기
try (FileReader reader = new FileReader("입력.txt")) {
    char[] 버퍼 = new char[1024];
    int 길이;
    while ((길이 = reader.read(버퍼)) != -1) {
        System.out.print(new String(버퍼, 0, 길이));
    }
} catch (IOException e) {
    e.printStackTrace();
}

// 파일에 문자 쓰기
try (FileWriter writer = new FileWriter("출력.txt")) {
    String 문자열 = "Hello World!";
    writer.write(문자열);
} catch (IOException e) {
    e.printStackTrace();
}

2.2 바이너리 데이터 읽기/쓰기

Java IO API를 사용해 바이너리 데이터를 읽고 쓸 수 있습니다. 주로 FileInputStream과 FileOutputStream을 사용합니다:

바이너리 데이터 읽기:

try (FileInputStream inputStream = new FileInputStream("입력.bin");
     FileOutputStream outputStream = new FileOutputStream("출력.bin")) {
    byte[] 버퍼 = new byte[1024];
    int 읽은바이트수;
    while ((읽은바이트수 = inputStream.read(버퍼)) != -1) {
        outputStream.write(버퍼, 0, 읽은바이트수);
    }
}

버퍼를 사용한 입출력:

try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream("입력.bin"));
     BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream("출력.bin"))) {
    byte[] 버퍼 = new byte[1024];
    int 읽은바이트수;
    while ((읽은바이트수 = inputStream.read(버퍼)) != -1) {
        outputStream.write(버퍼, 0, 읽은바이트수);
    }
}

2.3 객체 직렬화

Java에서 객체 직렬화(serialization)는 객체를 바이트 스트림으로 변환하는 과정입니다. 이를 통해 객체를 파일에 저장하거나 네트워크를 통해 전송할 수 있습니다. 역직렬화(deserialization)는 바이트 스트림을 다시 객체로 변환하는 과정입니다.

직렬화 예시:

import java.io.*;

public class 직렬화예제 {
    public static void main(String[] args) {
        // 객체 생성
        Person 사람 = new Person("Tom", 20);
        
        // 객체 파일에 쓰기
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("사람.ser"))) {
            oos.writeObject(사람);
            System.out.println("객체가 파일에 저장되었습니다.");
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 파일에서 객체 읽기
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("사람.ser"))) {
            Person 읽은사람 = (Person) ois.readObject();
            System.out.println("읽은 사람 정보: " + 읽은사람.toString());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    
    // 직렬화 가능한 Person 클래스
    static class Person implements Serializable {
        private String 이름;
        private int 나이;
        
        public Person(String 이름, int 나이) {
            this.이름 = 이름;
            this.나이 = 나이;
        }
        
        @Override
        public String toString() {
            return "Person{" +
                    "이름='" + 이름 + '\'' +
                    ", 나이=" + 나이 +
                    '}';
        }
    }
}

2.4 파일 작업

Java에서는 File 클래스를 사용해 파일을 다룰 수 있습니다. 주요 파일 작업은 다음과 같습니다:

  1. 파일 생성:
File 파일 = new File("새파일.txt");
if(파일.createNewFile()){
    System.out.println("파일이 생성되었습니다.");
}else{
    System.out.println("파일 생성에 실패했습니다.");
}
  1. 디렉토리 생성:
File 디렉토리 = new File("새디렉토리");
if(디렉토리.mkdirs()){
    System.out.println("디렉토리가 생성되었습니다.");
}else{
    System.out.println("디렉토리 생성에 실패했습니다.");
}
  1. 파일 복사, 이동, 삭제:
// 파일 복사
File 원본파일 = new File("원본.txt");
File 복사파일 = new File("복사본.txt");
try {
    Files.copy(원본파일.toPath(), 복사파일.toPath());
    System.out.println("파일이 복사되었습니다.");
} catch (IOException e) {
    e.printStackTrace();
}

// 파일 이동
File 이동파일 = new File("이동.txt");
try {
    Files.move(원본파일.toPath(), 이동파일.toPath());
    System.out.println("파일이 이동되었습니다.");
} catch (IOException e) {
    e.printStackTrace();
}

// 파일 삭제
File 삭제파일 = new File("삭제할파일.txt");
if(삭제파일.delete()){
    System.out.println("파일이 삭제되었습니다.");
}else{
    System.out.println("파일 삭제에 실패했습니다.");
}

2.5 정규 표현식

정규 표현식(Regular Expression)은 문자열의 패턴을 검색, 추출, 치환하는 데 사용되는 강력한 도구입니다. Java에서는 java.util.regex 패키지를 통해 정규 표현식을 사용할 수 있습니다.

주요 정규 표현식:

  1. .: 임의의 한 문자와 일치
  2. []: 문자 집합 중 하나와 일치
  3. *: 앞의 문자가 0번 이상 반복
  4. +: 앞의 문자가 1번 이상 반복
  5. ?: 앞의 문자가 0번 또는 1번 반복
  6. {n}: 앞의 문자가 정확히 n번 반복
  7. {n,}: 앞의 문자가 n번 이상 반복
  8. {n,m}: 앞의 문자가 n번 이상 m번 이하 반복
  9. ^: 문자열의 시작
  10. $: 문자열의 끝

정규 표현식 예시:

import java.util.regex.*;

public class 정규표현식예제 {
    public static void main(String[] args) {
        String 텍스트 = "The quick brown fox jumps over the lazy dog";
        
        // 패턴 매칭
        Pattern 패턴 = Pattern.compile("\\bfox\\b");
        Matcher 매처 = 패턴.matcher(텍스트);
        
        if(매처.find()) {
            System.out.println("패턴을 찾았습니다: " + 매처.group());
        }
        
        // 문자열 치환
        String 치환된텍스트 = 텍스트.replaceAll("fox", "cat");
        System.out.println("치환된 텍스트: " + 치환된텍스트);
        
        // 문자열 분할
        String[] 단어들 = 텍스트.split("\\s+");
        System.out.println("단어 개수: " + 단어들.length);
    }
}

3장 XML 처리

XML(Extensible Markup Language)은 데이터를 설명하기 위한 마크업 언어입니다. XML은 데이터 저장, 전송 및 표시에 사용되며, 다양한 시스템과 애플리케이션 간의 데이터 상호 운용성을 제공합니다. HTML과 유사하지만 XML의 문법은 더 엄격하며 사용자 정의 태그와 속성을 지원합니다.

XML 문서 구조는 요소(element), 속성(attribute), 텍스트 노드(text node)로 구성됩니다. XML 문서는 반드시 하나의 루트(root) 요소를 가져야 하며, 모든 다른 요소는 루트 요소 내에 중첩됩니다.

3.1 XML 개요

XML은 데이터 구조와 의미를 설명하기 위한 마크업 언어로, 1998년 W3C(World Wide Web Consortium)에 의해 도입되었습니다. HTML의 데이터 표현을 대체하기 위해 만들어졌으며, XML은 웹 페이지 표시가 아닌 데이터 교환과 저장에 사용됩니다.

XML의 핵심 설계 철학은 마크업을 사용자 정의할 수 있도록 하는 것입니다. 이는 XML이 높은 확장성과 유연성을 가지게 합니다. XML 마크업은 텍스트 요소이며, 텍스트, 이미지, 비디오, 오디오 및 구조화된 데이터 등 모든 유형의 데이터를 설명하는 데 사용할 수 있습니다.

3.2 XML 문서 구조

XML 문서는 다음과 같은 부분으로 구성됩니다:

  • 선언: XML 문서는 <?xml ?> 선언으로 시작하며, XML 버전 및 인코딩 방식 등을 정의합니다.
  • 루트 요소: XML 문서에는 반드시 하나의 루트 요소가 있으며, 모든 다른 요소와 속성이 이 안에 포함됩니다.
  • 요소: XML 요소는 시작 태그, 끝 태그 및 요소 내용으로 구성됩니다. 요소는 다른 요소와 텍스트 노드를 포함할 수 있습니다.
  • 속성: XML 속성은 요소 태그 내에 포함되며, 해당 요소의 추가 정보를 설명합니다.
  • 주석: XML 문서에는 주석을 포함할 수 있으며, 이는 문서를 읽는 사람에게 추가 정보나 설명을 제공합니다.
  • 처리 명령: XML 문서에는 처리 명령을 포함할 수 있으며, 이는 파서가 XML 문서를 처리하는 방법을 알려줍니다.

간단한 XML 문서 예시:

<?xml version="1.0" encoding="UTF-8"?>
<도서>
  <도서 id="001">
    <제목>Java 프로그래밍</제목>
    <저자>홍길동</저자>
    <가격>88.80</가격>
  </도서>
  <도서 id="002">
    <제목>Python 프로그래밍</제목>
    <저자>이순신</저자>
    <가격>68.50</가격>
  </도서>
</도서>

3.3 XML 파싱

XML 문서를 파싱하려면 파서(parser)를 사용해야 합니다. 주로 두 가지 방식이 있습니다:

  1. DOM 파서: XML 문서를 메모리에 로드하여 트리 구조를 형성하고, 노드를 통해 데이터를 추출합니다.
  2. SAX 파서: XML 문서를 한 줄씩 읽으며 특정 이벤트가 발생할 때 콜백 함수를 호출하여 데이터를 처리합니다.

DOM 파서 예시:

import javax.xml.parsers.*;
import org.w3c.dom.*;
import java.io.*;

public class DOM파서예제 {
    public static void main(String[] args) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(new File("도서.xml"));
            
            Element root = document.getDocumentElement();
            NodeList nodeList = root.getChildNodes();
            
            for (int i = 0; i < nodeList.getLength(); i++) {
                Node node = nodeList.item(i);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    Element element = (Element) node;
                    String tagName = element.getTagName();
                    // 태그명이 tagName인 요소의 값 추출
                    ...
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.4 XML 검증

XML 문서를 검증하려면 XML 검증기(validator)를 사용해야 합니다. XML 검증은 주로 DTD(Document Type Definition) 또는 XSD(XML Schema Definition)를 사용하여 수행됩니다.

XML 검증 예시:

import javax.xml.validation.*;
import javax.xml.*;
import org.xml.sax.*;
import java.io.*;

public class XML검증예제 {
    public static void main(String[] args) {
        try {
            // XSD 파일로 스키마 생성
            File xsdFile = new File("schema.xsd");
            SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
            Schema schema = schemaFactory.newSchema(xsdFile);
            
            // 검증기 생성
            Validator validator = schema.newValidator();
            
            // XML 파일 검증
            File xmlFile = new File("data.xml");
            validator.validate(new StreamSource(xmlFile));
            
            System.out.println("XML 문서가 유효합니다.");
        } catch (SAXException e) {
            System.out.println("XML 문서가 유효하지 않습니다: " + e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.5 XPath로 정보 위치 찾기

XPath는 XML 문서에서 요소를 찾기 위한 언어입니다. XPath를 사용하면 경로를 통해 쉽게 요소를 찾고 데이터를 추출할 수 있습니다.

XPath 예시:

import javax.xml.xpath.*;
import org.w3c.dom.*;
import javax.xml.parsers.*;
import java.io.*;

public class XPath예제 {
    public static void main(String[] args) throws Exception {
        // XPath 객체 생성
        XPath xpath = XPathFactory.newInstance().newXPath();
        
        // XML 문서 파싱
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document doc = builder.parse(new File("도서.xml"));
        
        // 모든 도서 요소 찾기
        NodeList bookNodes = (NodeList) xpath.evaluate("//도서", doc, XPathConstants.NODESET);
        for (int i = 0; i < bookNodes.getLength(); i++) {
            Node bookNode = bookNodes.item(i);
            // 도서 요소의 속성 가져오기
            String id = xpath.evaluate("@id", bookNode);
            String author = xpath.evaluate("저자", bookNode);
            String title = xpath.evaluate("제목", bookNode);
            String price = xpath.evaluate("가격", bookNode);
            System.out.println("도서 " + id + ": " + title + " by " + author + " 가격 " + price);
        }
    }
}

3.6 네임스페이스 사용

XML에서 네임스페이스는 요소와 속성 이름의 충돌을 방지하기 위해 사용됩니다. 다음은 XML 문서에 네임스페이스를 정의하는 방법입니다:

<root xmlns:접두사="네임스페이스URI">
  <접두사:요소>값</접두사:요소>
</root>

위 예제에서 xmlns:접두사 부분이 네임스페이스를 정의합니다. 접두사는 네임스페이스의 접두사이며, 뒤에 오는 네임스페이스URI는 네임스페이스의 고유 식별자입니다. root 요소에서는 접두사를 사용하여 요소를 정의했습니다.

3.7 스트림 기반 파서

스트림 기반 파서는 SAX( Simple API for XML)와 StAX(Streaming API for XML)가 있습니다. 이러한 파서는 전체 XML 문서를 메모리에 로드하지 않고도 처리할 수 있어 대용량 XML 파일에 적합합니다.

SAX 파서 예시:

import org.xml.sax.*;
import org.xml.sax.helpers.*;
import javax.xml.parsers.*;
import java.io.*;

public class SAX파서예제 {
    public static void main(String[] args) {
        try {
            // SAXParserFactory 생성
            SAXParserFactory factory = SAXParserFactory.newInstance();
            SAXParser saxParser = factory.newSAXParser();
            
            // 커스텀 핸들러 생성
            MyHandler handler = new MyHandler();
            
            // XML 파일 파싱
            saxParser.parse(new File("도서.xml"), handler);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyHandler extends DefaultHandler {
    // 시작 요소 발생 시 호출
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        System.out.print("<" + qName);
        // 속성 출력
        for (int i = 0; i < attributes.getLength(); i++) {
            System.out.print(" " + attributes.getQName(i) + "=\"" + attributes.getValue(i) + "\"");
        }
        System.out.print(">");
    }
    
    // 끝 요소 발생 시 호출
    public void endElement(String uri, String localName, String qName) throws SAXException {
        System.out.print("</" + qName + ">");
    }
    
    // 텍스트 내용 발생 시 호출
    public void characters(char[] ch, int start, int length) throws SAXException {
        System.out.print(new String(ch, start, length));
    }
}

3.8 XML 문서 생성

Java에서는 JAXP 라이브러리를 사용해 XML 문서를 생성할 수 있습니다. 다음은 간단한 XML 문서를 생성하는 예시입니다:

import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;
import java.io.*;

public class XML생성예제 {
    public static void main(String[] args) throws Exception {
        // DocumentBuilder 생성
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        
        // Document 생성
        Document doc = builder.newDocument();
        
        // 루트 요소 생성
        Element rootElement = doc.createElement("사용자");
        doc.appendChild(rootElement);
        
        // 자식 요소 user1 생성
        Element user1 = doc.createElement("사용자");
        user1.setAttribute("id", "1001");
        rootElement.appendChild(user1);
        
        Element name1 = doc.createElement("이름");
        name1.setTextContent("Alice");
        user1.appendChild(name1);
        
        Element age1 = doc.createElement("나이");
        age1.setTextContent("25");
        user1.appendChild(age1);
        
        // 자식 요소 user2 생성
        Element user2 = doc.createElement("사용자");
        user2.setAttribute("id", "1002");
        rootElement.appendChild(user2);
        
        Element name2 = doc.createElement("이름");
        name2.setTextContent("Bob");
        user2.appendChild(name2);
        
        Element age2 = doc.createElement("나이");
        age2.setTextContent("30");
        user2.appendChild(age2);
        
        // XML 문서 생성
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        
        // 출력 형식 지정
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        
        // 파일 생성
        DOMSource source = new DOMSource(doc);
        StreamResult result = new StreamResult(new File("사용자.xml"));
        transformer.transform(source, result);
        
        System.out.println("XML 파일이 생성되었습니다.");
    }
}

4장 네트워크 프로그래밍

네트워크 프로그래밍은 컴퓨터 네트워크를 통해 프로그램 간 데이터 교환과 통신을 구현하는 것입니다. Java에서 네트워크 프로그래밍은 주로 java.net과 java.io 패키지를 사용합니다. Java는 다양한 클래스와 인터페이스를 제공하여 안정적인 네트워크 애플리케이션을 개발할 수 있도록 지원합니다.

Java 네트워크 프로그래밍의 주요 개념과 구성 요소는 다음과 같습니다:

  1. IP 주소와 포트 번호: 네트워크 통신에서 컴퓨터는 IP 주소로 식별되며, 포트 번호는 프로세스 간 통신을 위한 포인트를 나타냅니다.
  2. 소켓(Socket): Java 네트워크 통신의 기본 단위로, 통신의 세부 사항을 정의합니다.
  3. 프로토콜: 네트워크 통신 과정의 규칙과 표준으로, TCP, UDP, HTTP, FTP 등이 있습니다.
  4. URL과 URLConnection: URL(Uniform Resource Locator)은 웹 리소스의 주소를 나타냅니다. URLConnection은 웹 서버와의 연결을 설정하는 데 사용됩니다.

Java 네트워크 프로그래밍은 주로 클라이언트와 서버 애플리케이션을 생성하는 데 사용됩니다. Socket과 ServerSocket 클래스를 사용하면 이 두 종류의 애플리케이션을 쉽게 구현할 수 있습니다. 클라이언트 애플리케이션에서는 Socket 클래스를 사용하여 서버에 연결합니다. 서버 측에서는 ServerSocket 클래스를 사용하여 클라이언트의 연결 요청을 수신하고 통신을 위한 소켓을 생성합니다.

4.1 서버 연결

서버에 연결하려면 원격 접속 프로토콜 중 하나를 사용해야 합니다. SSH(보안 셸 프로토콜), RDP(원격 데스크톱 프로토콜) 또는 VNC(원격 가상 컴퓨터) 등이 있습니다. 다음은 서버에 연결하는 단계입니다:

  1. 서버의 IP 주소 또는 도메인 이름을 얻습니다.
  2. 적절한 원격 접속 프로토콜을 선택합니다.
  3. 컴퓨터에서 해당 원격 접속 소프트웨어(PuTTY 또는 Microsoft Remote Desktop 등)를 엽니다.
  4. 서버의 IP 주소 또는 도메인 이름과 포트 번호(필요한 경우)를 입력합니다.
  5. 사용자 이름과 비밀번호를 입력하여 서버에 로그인합니다.
  6. 연결이 성공하면 원격 접속 소프트웨어를 통해 서버에 접속하고 관리할 수 있습니다.

4.2 서버 구현

서버를 구현하려면 다음 단계를 따릅니다:

  1. 운영체제와 프로그래밍 언어를 결정합니다. 일반적으로 사용되는 운영체제는 Windows, Linux, Unix 등이며, 프로그래밍 언어는 Java, Python, C++, Node.js 등이 있습니다.
  2. 서버 코드를 작성합니다. 코드에는 웹 서버, 메일 서버, 파일 서버 등 구현하려는 기능이 포함되어야 합니다.
  3. 서버의 포트를 지정합니다. 서버는 특정 포트에서 클라이언트 요청을 수신해야 하며, 일반적으로 80(HTTP), 443(HTTPS), 25(SMTP) 등의 표준 포트가 사용됩니다.
  4. 서버의 네트워크 및 보안 설정을 구성합니다. 클라이언트가 서버에 접근할 수 있도록 해야 하며, 적절한 방화벽 및 보안 조치를 적용하여 서버를 보호해야 합니다.
  5. 서버 코드를 실행합니다. 로컬 컴퓨터에서 서버 코드를 실행하여 테스트하거나, 클라우드 서비스나 호스팅 서비스 제공업체에 배포하여 실제 사용할 수 있습니다.
  6. 클라이언트가 서버에 연결합니다. 클라이언트는 서버의 IP 주소와 포트를 알아야 서버에 연결하고 요청을 보낼 수 있습니다.
  7. 클라이언트 요청을 처리합니다. 서버는 클라이언트의 요청에 따라 적절한 작업을 수행하고 결과를 클라이언트에 반환해야 합니다.
  8. 서버의 성능 및 보안을 모니터링합니다. 정기적으로 서버의 성능과 보안을 모니터링하고, 소프트웨어와 패치를 업데이트하여 서버의 안정성을 유지해야 합니다.

4.3 웹 데이터 가져오기

웹 데이터를 가져오는 방법은 다음과 같습니다:

  1. 웹 크롤링: Python의 Requests, BeautifulSoup, Scrapy 등의 크롤링 프로그램을 사용하여 웹 페이지를 자동으로 방문하고 데이터를 추출합니다.
  2. API 호출: Google Maps API, Twitter API 등 웹 API를 통해 온라인 서비스가 제공하는 데이터에 접근합니다. API는 일반적으로 RESTful 형식으로 데이터를 제공합니다.
  3. 데이터베이스 쿼리: 자주 접근해야 하는 데이터는 데이터베이스에 저장하고 SQL 쿼리 문을 통해 데이터를 검색할 수 있습니다.
  4. RSS 구독: 관심 있는 콘텐츠를 RSS 리더에 구독하고, RSS가 제공하는 XML 데이터를 통해 최신 정보를 얻을 수 있습니다.

4.4 HTTP 클라이언트

HTTP 클라이언트는 HTTP 서버에 요청을 보내고 응답을 수신하는 소프트웨어 프로그램입니다. 웹 브라우저는 일종의 HTTP 클라이언트입니다. Java에서는 다음과 같은 HTTP 클라이언트를 사용할 수 있습니다:

  1. HttpURLConnection 클래스: Java 표준 라이브러리에 포함된 HTTP 클라이언트로, HTTP 요청을 보내고 응답을 받을 수 있습니다.
  2. Apache HttpClient: 더 많은 기능과 유연성을 제공하는 서드파티 라이브러리로, HTTPS, 연결 풀, 쿠키 관리 등을 지원합니다.
  3. OkHttp: HTTP/2, 캐싱, 연결 풀 등 완전한 기능을 제공하는 또 다른 서드파티 라이브러리입니다.

HttpURLConnection을 사용한 HTTP GET 요청 예시:

import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class HTTP클라이언트예제 {
    public static void main(String[] args) throws Exception {
        URL url = new URL("https://www.example.com");
        HttpURLConnection con = (HttpURLConnection) url.openConnection();
        con.setRequestMethod("GET");
        con.setConnectTimeout(5000);
        con.setReadTimeout(5000);

        BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
        String inputLine;
        StringBuilder content = new StringBuilder();
        while ((inputLine = in.readLine()) != null) {
            content.append(inputLine);
        }
        in.close();

        System.out.println(content);
    }
}

4.5 이메일 발송

Java에서는 JavaMail API를 사용하여 이메일을 발송할 수 있습니다. JavaMail API는 이메일을 발송하고 수신하기 위한 Java API입니다. 다음은 이메일을 발송하는 예제 코드입니다:

import java.util.Properties;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

public class 이메일발송예제 {
    public static void main(String[] args) {
        // SMTP 서버 주소
        String smtpHost = "smtp.gmail.com";
        // 발신자 이메일 주소
        String fromEmail = "yourEmailAddress@gmail.com";
        // 발신자 이메일 비밀번호 또는 인증 코드
        String fromPassword = "yourEmailPassword";
        // 수신자 이메일 주소
        String toEmail = "recipientEmailAddress@gmail.com";

        // 이메일 구성
        Properties props = new Properties();
        props.put("mail.smtp.host", smtpHost);
        props.put("mail.smtp.port", "587");
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.starttls.enable", "true");

        // 세션 생성
        Session session = Session.getInstance(props);

        try {
            // 이메일 생성
            Message msg = new MimeMessage(session);
            msg.setFrom(new InternetAddress(fromEmail));
            msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail));
            msg.setSubject("테스트 이메일");
            msg.setText("이것은 테스트 이메일입니다.");

            // 이메일 발송
            Transport.send(msg, fromEmail, fromPassword);

            System.out.println("이메일이 성공적으로 발송되었습니다.");
        } catch (MessagingException e) {
            e.printStackTrace();
        }
    }
}

이 예제에서는 세션 객체를 생성하고 SMTP 서버의 구성 정보를 전달합니다. 그런 다음 MimeMessage 객체를 생성하여 발신자, 수신자, 제목, 본문을 설정합니다. 마지막으로 Transport.send() 메소드를 호출하여 이메일을 발송합니다. Transport.send() 메소드를 호출할 때 발신자의 이메일 주소와 비밀번호 또는 인증 코드를 전달해야 합니다.

태그: Java 8 스트림 API 입출력 처리 XML 처리 네트워크 프로그래밍

6월 29일 04:15에 게시됨