728x90
반응형

Java NIO (New I/O)는 Java 1.4 버전에서 도입된 **비동기 I/O (Asynchronous I/O)**를 지원하는 강력한 I/O 라이브러리입니다. 특히, 대규모 네트워크 서버파일 시스템과의 I/O 처리가 중요한 애플리케이션에서 매우 유용합니다. Java NIO의 가장 큰 특징은 non-blocking I/O, 버퍼 (Buffer), 채널 (Channel), 셀렉터 (Selector) 등의 개념을 제공하여 높은 성능과 효율적인 자원 관리를 가능하게 만든다는 점입니다.

이번에는 Java NIO의 고급 사용법비동기 처리, 파일 처리, 서버 구현 등의 측면에서 심도 깊게 다뤄보겠습니다.


Java NIO 주요 개념

  1. Channel:
    • NIO에서 데이터의 입출력 작업을 수행하는 통로입니다.
    • FileChannel, SocketChannel, DatagramChannel, ServerSocketChannel 등이 있습니다.
    • NIO의 채널은 기본적으로 non-blocking 모드로 동작할 수 있어, 서버 애플리케이션에서 더 많은 클라이언트를 동시에 처리할 수 있습니다.
  2. Buffer:
    • 데이터는 Buffer 객체를 통해 읽기쓰기가 이루어집니다.
    • ByteBuffer, CharBuffer, IntBuffer 등 여러 종류가 있으며, 기본적인 버퍼링 기능을 제공합니다.
    • 버퍼는 데이터를 읽거나 쓸 때 position, limit, capacity 등을 통해 제어합니다.
  3. Selector:
    • 여러 채널을 감시하고, I/O 작업을 비동기적으로 처리할 수 있는 메커니즘입니다.
    • 여러 채널에서 입력/출력 이벤트가 발생했을 때 해당 이벤트를 **선택(select)**하여 처리합니다.
    • Selector는 non-blocking I/O 환경에서 매우 중요한 역할을 합니다.

Java NIO 고급 사용법 예시

1. NIO를 사용한 고성능 파일 I/O 처리

Java NIO의 FileChannel을 사용하면 파일을 비동기적으로 읽고 쓸 수 있습니다. **메모리 맵핑 (Memory-mapping)**을 활용하면 파일 데이터를 메모리처럼 직접 다룰 수 있어 성능을 극대화할 수 있습니다.

예시: FileChannel을 이용한 대용량 파일 읽기 및 쓰기

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

public class FileChannelExample {
    public static void main(String[] args) throws IOException {
        // 큰 파일을 읽고 쓰는 예시
        File inputFile = new File("largeInputFile.txt");
        File outputFile = new File("largeOutputFile.txt");

        try (FileChannel inputChannel = new FileInputStream(inputFile).getChannel();
             FileChannel outputChannel = new FileOutputStream(outputFile).getChannel()) {

            // 메모리 맵핑을 이용한 파일 읽기
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            while (inputChannel.read(buffer) > 0) {
                buffer.flip();  // 버퍼를 읽기 모드로 설정
                outputChannel.write(buffer);
                buffer.clear();  // 버퍼를 다시 쓰기 모드로 설정
            }

            System.out.println("File copy completed.");
        }
    }
}

핵심 포인트:

  • FileChannel은 대용량 파일 처리에 매우 효율적입니다.
  • ByteBuffer를 사용하여 데이터를 버퍼링하고, I/O 성능을 최적화할 수 있습니다.

2. Non-blocking 서버 소켓 구현 (ServerSocketChannel과 Selector)

NIO의 ServerSocketChannelSelector를 활용하면, 비동기 방식으로 다수의 클라이언트 요청을 동시에 처리할 수 있습니다. Selector는 여러 채널을 동시에 감시할 수 있기 때문에, 하나의 스레드로 다수의 클라이언트 요청을 처리할 수 있습니다.

예시: Non-blocking 서버 소켓 구현

import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.util.*;

public class NioServer {
    public static void main(String[] args) throws IOException {
        // 서버 소켓 채널을 열고 설정
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        serverChannel.bind(new InetSocketAddress(8080));

        // 셀렉터 열기
        Selector selector = Selector.open();
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            // 셀렉터에서 이벤트가 발생할 때까지 대기
            selector.select();

            // 셀렉터에 등록된 키를 순회하면서 이벤트 처리
            Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
            while (keys.hasNext()) {
                SelectionKey key = keys.next();
                keys.remove();

                if (key.isAcceptable()) {
                    // 클라이언트 연결 수락
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("New client connected: " + client.getRemoteAddress());
                } else if (key.isReadable()) {
                    // 클라이언트로부터 데이터 읽기
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(256);
                    int bytesRead = client.read(buffer);
                    if (bytesRead == -1) {
                        client.close();
                    } else {
                        buffer.flip();
                        client.write(buffer);  // 데이터를 다시 클라이언트로 전송
                    }
                }
            }
        }
    }
}

핵심 포인트:

  • ServerSocketChannelSelector를 사용하여 non-blocking 서버를 구현합니다.
  • Selector는 여러 입력/출력 이벤트를 처리하며, 하나의 스레드로 여러 클라이언트를 동시에 처리할 수 있습니다.
  • 클라이언트 요청을 비동기적으로 처리하여 성능을 최적화합니다.

3. Java NIO를 활용한 비동기 네트워크 클라이언트

NIO를 사용하면 클라이언트에서도 non-blocking I/O 방식으로 네트워크 통신을 할 수 있습니다. 이를 통해 대기 시간 없이 다른 작업을 병렬로 처리할 수 있습니다.

예시: Non-blocking 클라이언트 구현

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.net.*;

public class NioClient {
    public static void main(String[] args) throws IOException {
        // 클라이언트 소켓 채널 열기
        SocketChannel clientChannel = SocketChannel.open();
        clientChannel.configureBlocking(false);
        clientChannel.connect(new InetSocketAddress("localhost", 8080));

        // 셀렉터 열기
        Selector selector = Selector.open();
        clientChannel.register(selector, SelectionKey.OP_CONNECT);

        while (true) {
            // 셀렉터에서 이벤트 대기
            selector.select();

            // 셀렉터의 키를 순회
            Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
            while (keys.hasNext()) {
                SelectionKey key = keys.next();
                keys.remove();

                if (key.isConnectable()) {
                    SocketChannel channel = (SocketChannel) key.channel();
                    if (channel.isConnectionPending()) {
                        channel.finishConnect();  // 연결 완료
                        System.out.println("Connected to server.");
                    }
                    channel.register(selector, SelectionKey.OP_WRITE);
                } else if (key.isWritable()) {
                    // 서버에 데이터 전송
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(256);
                    buffer.put("Hello, Server!".getBytes());
                    buffer.flip();
                    channel.write(buffer);
                    System.out.println("Message sent to server.");
                    channel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    // 서버로부터 데이터 수신
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(256);
                    int bytesRead = channel.read(buffer);
                    if (bytesRead == -1) {
                        channel.close();
                    } else {
                        buffer.flip();
                        String response = new String(buffer.array(), 0, bytesRead);
                        System.out.println("Received from server: " + response);
                    }
                }
            }
        }
    }
}

핵심 포인트:

  • SocketChannel을 사용하여 non-blocking 방식으로 서버에 연결하고 데이터를 주고받습니다.
  • Selector를 사용하여 I/O 이벤트를 처리하며, 메인 스레드에서 다른 작업을 수행하면서도 클라이언트와 서버 간의 통신을 비동기적으로 처리합니다.

NIO를 활용한 고급 팁

  1. 메모리 맵핑 (Memory-mapped Files):
    • **MappedByteBuffer**를 사용하면 파일을 메모리에 직접 매핑하여 매우 빠른 파일 처리가 가능합니다. 이는 대용량 파일을 처리하는 애플리케이션에서 속도효율성을 크게 개선할 수 있습니다.
  2. Selector와 여러 채널을 동시에 관리:
    • 여러 네트워크 채널, 파일 채널을 동시에 처리해야 하는 경우, SelectorMultiplexing 기법을 활용하여 스레드 수를 최소화하고 시스템 성능을 최적화할 수 있습니다.
  3. 배치 작업 처리:
    • Batch I/O operations: 여러 개의 I/O 작업을 한 번에 처리하거나, 특정 시점에 한 번에 모든 데이터를 읽거나 쓸 수 있습니다. 이를 통해 I/O 성능을 최적화할 수 있습니다.

정리

Java NIO는 비동기 I/O, Selector, Channel, Buffer 등을 활용하여 고성능 서버대규모 I/O 처리를 할 수 있도록 해줍니다. 이를 통해 단일 스레드로도 많은 클라이언트를 처리할 수 있으며, 성능을 크게 개선할 수 있습니다. Java NIO는 대용량 데이터 처리고성능 네트워크 서버에서 매우 유용하게 사용됩니다.

728x90
반응형
728x90
반응형

writeObject와 readObject 메서드를 통한 커스텀 직렬화는 Java에서 객체를 직렬화하고 역직렬화할 때, 기본 직렬화 방식의 동작을 변경하거나 제어할 수 있게 해주는 강력한 기능입니다. 이를 통해 데이터의 직렬화 과정커스터마이즈할 수 있으며, 성능 최적화보안을 강화하거나 특정 필드를 제외하는 등의 작업을 할 수 있습니다.

직렬화와 역직렬화 기본 개념

  • 직렬화 (Serialization): 객체를 바이트 스트림으로 변환하여 파일, 네트워크 전송 등을 할 수 있게 만듭니다.
  • 역직렬화 (Deserialization): 바이트 스트림을 다시 원래의 객체 형태로 복원하는 과정입니다.

커스텀 직렬화

Java에서 기본적으로 Serializable 인터페이스를 구현하면 객체는 자동으로 직렬화됩니다. 그러나 writeObject와 readObject 메서드를 사용하면 직렬화/역직렬화 과정을 커스터마이즈할 수 있습니다.

  • writeObject(ObjectOutputStream out) 메서드: 객체를 직렬화할 때 호출됩니다.
  • readObject(ObjectInputStream in) 메서드: 객체를 역직렬화할 때 호출됩니다.

주요 목적

  • 특정 필드를 직렬화에서 제외하거나 포함할 수 있습니다.
  • 직렬화 과정에서 암호화압축을 추가하거나, 성능 최적화를 할 수 있습니다.
  • 객체의 버전 관리 (예: 클래스 변경 시 이전 버전 객체의 역직렬화 처리)를 할 수 있습니다.

예시 1: 필드 제외 및 커스터마이즈

transient 키워드를 사용하면 직렬화에서 특정 필드를 제외할 수 있지만, writeObject와 readObject를 사용하면 더 세밀한 제어가 가능합니다.

커스터마이즈 예시: 특정 필드 제외

import java.io.*;

class Person implements Serializable {
    private String name;
    private transient int age;  // 직렬화에서 제외할 필드
    private String address;

    public Person(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject(); // 기본 직렬화
        // 'age'는 직렬화에서 제외
        oos.writeInt(age); // 커스텀 직렬화: 'age'는 별도로 저장
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject(); // 기본 역직렬화
        // 'age'를 역직렬화
        this.age = ois.readInt(); // 커스텀 역직렬화: 'age' 복원
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + ", address='" + address + "'}";
    }
}

public class CustomSerializationExample {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 객체 직렬화
        Person person = new Person("John", 30, "1234 Elm St");
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
            oos.writeObject(person);
        }

        // 객체 역직렬화
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {
            Person deserializedPerson = (Person) ois.readObject();
            System.out.println("Deserialized Person: " + deserializedPerson);
        }
    }
}

핵심 포인트:

  • transient로 필드를 제외할 수도 있지만, writeObject와 readObject를 사용하여 별도로 제어할 수 있습니다.
  • age 필드는 transient로 제외되었으므로 기본 직렬화에서는 저장되지 않으며, 커스터마이즈된 방식으로 저장되고 복원됩니다.

예시 2: 버전 관리 및 호환성

Java 직렬화에서는 클래스의 버전 관리가 중요합니다. serialVersionUID 필드를 사용하여 클래스의 버전 호환성을 관리할 수 있습니다. 그러나 writeObject와 readObject를 사용하면, 버전 변경 시 이전 버전 객체의 역직렬화를 처리할 수 있습니다.

커스터마이즈 예시: 객체 버전 관리

import java.io.*;

class PersonV1 implements Serializable {
    private static final long serialVersionUID = 1L;  // 첫 번째 버전

    private String name;
    private int age;

    public PersonV1(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();
        // V1에서 V2로 버전 변경된 필드를 처리할 수 있습니다.
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
    }

    @Override
    public String toString() {
        return "PersonV1{name='" + name + "', age=" + age + "}";
    }
}

class PersonV2 implements Serializable {
    private static final long serialVersionUID = 2L;  // 두 번째 버전

    private String name;
    private int age;
    private String address;  // 새로운 필드 추가

    public PersonV2(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();  // V2의 직렬화
        // 추가된 'address' 필드 직렬화
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        // V1에서 V2로 변환 시, address 필드를 기본값으로 설정하거나 다른 처리할 수 있음
    }

    @Override
    public String toString() {
        return "PersonV2{name='" + name + "', age=" + age + "', address='" + address + "'}";
    }
}

public class VersionedSerializationExample {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // PersonV1 직렬화
        PersonV1 personV1 = new PersonV1("Alice", 28);
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("personV1.ser"))) {
            oos.writeObject(personV1);
        }

        // PersonV2 역직렬화 (기존 V1 객체를 V2로 읽기)
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("personV1.ser"))) {
            PersonV2 personV2 = (PersonV2) ois.readObject();
            System.out.println("Deserialized PersonV2: " + personV2);
        }
    }
}

핵심 포인트:

  • 객체 버전 관리에서 중요한 점은 기존 버전 객체를 새로운 버전으로 변환할 수 있어야 한다는 것입니다.
  • writeObject와 readObject를 활용하여 새로운 필드변경된 필드를 처리할 수 있습니다.
  • serialVersionUID는 클래스 버전 관리에 도움을 줍니다. 만약 클래스가 변경되어도, 호환성 있게 객체를 역직렬화할 수 있습니다.

예시 3: 객체 내부 상태 제어 (압축, 암호화)

직렬화/역직렬화 과정에서 암호화압축을 추가하여 데이터를 보호하거나 성능을 최적화할 수 있습니다.

커스터마이즈 예시: 압축 및 암호화

import java.io.*;
import java.util.zip.*;
import java.security.*;

class SecurePerson implements Serializable {
    private String name;
    private int age;

    public SecurePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        // 압축된 직렬화
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        try (ObjectOutputStream tempOos = new ObjectOutputStream(byteStream)) {
            tempOos.writeObject(this);
        }

        // 압축
        try (GZIPOutputStream gzip = new GZIPOutputStream(oos)) {
            gzip.write(byteStream.toByteArray());
        }
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        // 압축된 데이터 해제
        try (GZIPInputStream gzip = new GZIPInputStream(ois);
             ObjectInputStream tempOis = new ObjectInputStream(gzip)) {
            SecurePerson person = (SecurePerson) tempOis.readObject();
            this.name = person.name;
            this.age = person.age;
        }
    }

    @Override
    public String toString() {
        return "SecurePerson{name='" + name + "', age=" + age + "}";
    }
}

public class SecureSerializationExample {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 객체 직렬화 (압축 및 암호화)
        SecurePerson person = new SecurePerson("Bob", 40);
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("securePerson.ser"))) {
            oos.writeObject(person);
        }

        // 객체 역직렬화 (압축 해제 및 복원)
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("securePerson.ser"))) {
            SecurePerson deserializedPerson = (SecurePerson) ois.readObject();
            System.out.println("Deserialized SecurePerson: " + deserializedPerson);
        }
    }
}

핵심 포인트:

  • 직렬화 데이터에 압축이나 암호화를 추가하여 보안성이나 성능 최적화를 강화할 수 있습니다.
  • writeObject와 readObject에서 압축/해제 또는 암호화/복호화 처리를 추가할 수 있습니다.

정리

  • **writeObject**와 **readObject**는 커스터마이즈된 직렬화역직렬화 과정을 통해 객체의 직렬화 동작을 세밀하게 제어할 수 있습니다.
  • 이를 사용하여 특정 필드 제외, 버전 관리, 압축 및 암호화 등의 다양한 기능을 구현할 수 있습니다.
  • 직렬화와 역직렬화 과정에서의 보안, 성능 최적화데이터 무결성 등을 더 잘 다룰 수 있는 강력한 도구입니다.
728x90
반응형
728x90
반응형

✅ 1. Idempotency (중복 처리 방지)

💡 목적

MQTT는 QoS 1/2 사용 시 메시지가 중복 전송될 수 있으므로
Kafka Consumer나 DB 처리 단계에서 중복 방지가 필요합니다.

✅ 전략

  • MQTT 메시지에 messageId 또는 eventId 필드 포함 (UUID 등)
  • Kafka Consumer가 해당 ID를 기준으로 처리 여부 조회
  • 이미 처리된 경우 skip (DB 또는 Redis 등에 처리 이력 저장)

✅ 예시

{
  "messageId": "c2e2a8dd-52aa-403f-bb6d-1a7a2eeac341",
  "deviceId": "abc123",
  "timestamp": "2025-06-27T10:30:00Z",
  "data": { "temp": 25.3 }
}

✅ 처리 예 (Kafka Consumer):

if (messageRepository.existsByMessageId(message.getMessageId())) {
    log.info("Duplicate message: {}", message.getMessageId());
    return; // 중복 skip
}

messageRepository.save(message); // 최초 처리

※ 또는 Redis에 messageId를 TTL과 함께 저장하여 빠르게 검사


✅ 2. Topic Routing (MQTT → Kafka 토픽 매핑)

💡 목적

MQTT의 토픽 구조(/device/abc/temp)를 Kafka에 맞는 구조(device.abc.temp)로 변환

✅ 기본 전략

  • / → . 치환 또는
  • 패턴 기반 변환: 정규식 or prefix 매핑

✅ 예시 구현

public String mapMqttToKafkaTopic(String mqttTopic) {
    return mqttTopic.replace("/", ".");
}

또는 더 복잡한 경우:

if (mqttTopic.matches("/device/[^/]+/temp")) {
    return "device.temp";
} else if (mqttTopic.matches("/sensor/.+")) {
    return "sensor.data";
} else {
    return "unknown.topic";
}

✅ 3. DLQ (Dead Letter Queue) 처리

💡 목적

Kafka publish나 Consumer 처리 실패 시 메시지를 별도 토픽(DLQ)으로 보내고
문제 분석 또는 재처리 대상으로 격리

✅ 구현 전략 (Kafka publish 실패 시):

try {
    kafkaTemplate.send(topic, message).get(); // 동기 전송 (예외 캐치용)
} catch (Exception e) {
    log.error("Kafka publish failed: {}", e.getMessage());
    kafkaTemplate.send("deadletter.mqtt", message);
}

deadletter.mqtt는 운영 Kafka 클러스터에서 모니터링하거나 Alert 대상으로 사용 가능

✅ Kafka Consumer → DLQ 처리 예시도 가능:

Spring Kafka에서 ErrorHandler를 통해 DLQ 자동 라우팅 설정 가능


✅ 4. JWT / TLS 기반 MQTT 보안 설정

💡 목적

MQTT Broker(예: EMQX, Mosquitto 등)와의 통신 시 기기 인증과 전송 보안을 확보


✅ TLS 인증 (Broker 측 설정)

EMQX 예시:

listener.ssl.external {
  enable: true
  bind: 8883
  keyfile: etc/certs/server.key
  certfile: etc/certs/server.crt
  cacertfile: etc/certs/ca.crt
}

Spring MQTT Client 설정:

java
복사편집
MqttConnectOptions options = new MqttConnectOptions(); options.setSocketFactory(SSLSocketFactoryFactory.get("ca.crt", "client.crt", "client.key"));

직접 인증서 기반 인증(TLS mutual authentication)


✅ JWT 인증 (EMQX)

  1. JWT를 username 또는 password 필드에 담아 전송
  2. EMQX에서 JWT 인증 플러그인 활성화
  3. PEM 키 또는 JWKS URL 기반 서명 검증

Spring MQTT 설정:

MqttConnectOptions options = new MqttConnectOptions();
options.setSocketFactory(SSLSocketFactoryFactory.get("ca.crt", "client.crt", "client.key"));

📌 종합 구조 예시

options.setUserName("jwt");
options.setPassword("<jwt_token>".toCharArray());

📝 요약


 

항목 구현 포인트 도구/구현 위치
Idempotency messageId → Redis/DB로 중복검사 Kafka Consumer
Topic Routing MQTT topic → Kafka topic 규칙 변환 Spring App 또는 EMQX
DLQ 처리 Kafka publish 실패 시 fallback KafkaTemplate try-catch
보안 연동 TLS 인증서, JWT token 사용 MQTT Client + Broker 설정

 

728x90
반응형
728x90
반응형

🧭 요약: Kafka가 하는 역할

"실시간 수신 데이터를 분산 처리 가능한 구조로 중계해주는 중심 허브 역할"


🔍 Kafka의 역할 상세 정리

1. 비동기 처리 (Decoupling)

  • WebSocket 서버는 데이터를 즉시 Kafka에 넣고 종료 (빠름)
  • 이후 처리 로직(MariaDB 저장, 알림 발송, 로그 기록 등)은 Kafka consumer들이 비동기적으로 수행

✅ WebSocket 서버의 부담이 줄고, 처리 지연 없이 응답 가능
✅ 소비 로직은 느려도 상관없음


2. 데이터 버퍼/완충지 역할 (Buffering)

  • 순간적으로 데이터 폭주가 발생해도 Kafka가 데이터를 큐 형태로 저장해서 순차 처리 가능
  • 예: 1초에 수천 개 메시지가 들어와도 consumer가 순차 처리

✅ 시스템 안정성 증가
✅ 장애 시 메시지 유실 방지 (Kafka는 디스크에 저장)


3. 데이터 전달 허브 (Routing)

  • 여러 consumer들이 동일한 topic을 구독할 수 있음
  • 예:
    • consumer A: MariaDB 저장
    • consumer B: 실시간 대시보드 반영
    • consumer C: 이상 탐지 분석

✅ 하나의 데이터로 여러 기능 분기 가능
✅ 마이크로서비스 구조와 궁합 좋음


4. 재처리 가능성 확보 (Reprocessing)

  • Kafka는 데이터를 로그처럼 저장하기 때문에,
  • 일정 시간(예: 7일) 내에는 이전 메시지 재처리 가능

✅ 예전 데이터를 다시 분석하거나 저장 가능
✅ 소비자가 장애났다가 복구되어도 처리 재시도 가능


5. 확장성 (Scalability)

  • Kafka는 topic을 partition으로 나누고,
  • consumer group을 늘려서 병렬 소비 가능

✅ 대규모 IoT/임베디드 데이터에도 대응 가능
✅ 소비자가 여러 인스턴스로 scale-out 가능


🔁 비교: Kafka 없이 직접 DB 저장했을 경우

항목 Kafka 미사용 Kafka 사용
WebSocket 서버 처리 부담 높음 낮음
장애 전파 범위 큼 (DB/네트워크 장애 시 전체 영향) 작음 (consumer만 영향)
실시간 분석/대시보드 연동 구현 복잡 여러 consumer로 분리 가능
확장성 낮음 높음
재처리 어려움 가능 (offset 기반)
 

🧪 실무 예시

Kafka Topic 구독 Consumer
device.data MariaDB 저장 서비스
device.alert 알림 시스템 (FCM, Slack)
device.log 로그/감사 이력 저장 시스템
device.analytics Spark/Flink 스트리밍 분석 시스템
 

📌 결론

Kafka는 단순히 “전송해주는 애”가 아니라 다음과 같은 역할을 수행합니다:

✅ 빠르게 받고
✅ 큐잉하고
✅ 다양한 서비스로 나눠 보내고
✅ 장애를 막고
✅ 대용량을 감당하며
✅ 재처리까지 가능한 통합 메시지 허브

728x90
반응형
728x90
반응형

WebSocket → Kafka → Redis + MariaDB까지 저장하는 구조를 포함하면 다음과 같은 전체 파이프라인을 구성할 수 있습니다.


🎯 아키텍처 구성도

┌────────────────────┐
│  Embedded Device   │  ← WebSocket →
│  (Windows, Client) │
└─────────┬──────────┘
          ▼
┌──────────────────────────┐
│ Spring WebFlux Server    │
│  ┌────────────────────┐  │
│  │ WebSocketHandler   │◄─┐
│  └────────────────────┘  │
│         │                │
│         ▼                │
│ ┌──────────────────────┐ │
│ │ Kafka Producer       │ ├────────► Kafka Topic
│ └──────────────────────┘ │
│         │                │
│         ▼                │
│ ┌──────────────────────┐ │
│ │ RedisService         │ │ (상태 캐시)
│ └──────────────────────┘ │
│         │                │
│         ▼                │
│ ┌──────────────────────┐ │
│ │ MariaDB Repository   │ │ (데이터 영구 저장)
│ └──────────────────────┘ │
└──────────────────────────┘

🧱 1. WebSocket + Kafka + Redis + MariaDB 저장 예제

✅ 주요 기술 스택

  • Spring WebFlux
  • Spring Kafka
  • Spring Data Redis
  • Spring Data JPA (with MariaDB)
  • Project Reactor (Mono, Flux)
  • Embedded Device는 WebSocket 클라이언트 (예: Python, JS 등)

📦 2. 예제 도메인: DeviceData

@Entity
@Table(name = "device_data")
public class DeviceData {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String deviceId;
    private String payload;
    private LocalDateTime receivedAt;
}

🧩 3. Repository 인터페이스

@Repository
public interface DeviceDataRepository extends ReactiveCrudRepository<DeviceData, Long> {
}

또는 JpaRepository<DeviceData, Long> 사용 시 WebMVC 필요 → WebFlux와 JPA 혼용 시에는 @Transactional 주의.


🧪 4. WebSocketHandler 전체 예시

@Component
@RequiredArgsConstructor
public class DeviceWebSocketHandler implements WebSocketHandler {

    private final KafkaService kafkaService;
    private final RedisService redisService;
    private final DeviceDataService dataService;

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        String deviceId = session.getId(); // 혹은 클라이언트에서 별도 전송

        return session.receive()
                .map(WebSocketMessage::getPayloadAsText)
                .flatMap(payload -> {
                    // 1. Redis 상태 저장
                    redisService.saveDeviceStatus(deviceId, payload);

                    // 2. Kafka 전송
                    kafkaService.sendToKafka("device.topic", payload);

                    // 3. MariaDB 저장
                    return dataService.save(deviceId, payload);
                })
                .then();
    }
}

⚙️ 5. 서비스: Kafka + Redis + MariaDB

KafkaService

@Service
@RequiredArgsConstructor
public class KafkaService {
    private final KafkaTemplate<String, String> kafkaTemplate;

    public void sendToKafka(String topic, String message) {
        kafkaTemplate.send(topic, message);
    }
}

RedisService

@Service
@RequiredArgsConstructor
public class RedisService {
    private final RedisTemplate<String, String> redisTemplate;

    public void saveDeviceStatus(String deviceId, String payload) {
        redisTemplate.opsForValue().set("device:" + deviceId, payload);
    }
}

DeviceDataService (MariaDB 저장)

@Service
@RequiredArgsConstructor
public class DeviceDataService {

    private final DeviceDataRepository repository;

    public Mono<Void> save(String deviceId, String payload) {
        DeviceData data = new DeviceData();
        data.setDeviceId(deviceId);
        data.setPayload(payload);
        data.setReceivedAt(LocalDateTime.now());
        return Mono.fromRunnable(() -> repository.save(data)); // 비동기 래핑
    }
}

🧰 6. application.yml 설정 예시

spring:
  datasource:
    url: jdbc:mariadb://localhost:3306/devicedb
    username: youruser
    password: yourpassword
  redis:
    host: localhost
    port: 6379
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer

📎 7. Gradle 의존성

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.kafka:spring-kafka'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.mariadb.jdbc:mariadb-java-client'
    implementation 'io.projectreactor:reactor-core'
}

✅ 구성 팁 및 고려사항

항목 권장
WebSocket 연결 보안 wss:// (TLS) 적용
MariaDB 저장 실패 대응 @Retryable 또는 Kafka consumer fallback 사용
Kafka 사용 목적 메시지 브로커 또는 로그/처리 분리용
Redis 목적 상태 관리, TTL, 세션 추적
백프레셔 .onBackpressureBuffer()로 처리 제한 고려
데이터 포맷 JSON 포맷 표준화 권장 (Jackson ObjectMapper)
 

🎁 보너스: WebSocket 클라이언트 예 (Python)

import websocket

def on_message(ws, message):
    print("Received:", message)

ws = websocket.WebSocketApp("ws://localhost:8080/ws/device", on_message=on_message)
ws.on_open = lambda ws: ws.send("TEMP:22.5,HUM:50.1")
ws.run_forever()

🏁 마무리

이 구조는 다음의 목적을 모두 충족합니다:

  • 임베디드 장치에서 실시간 데이터 수신 (WebSocket)
  • Kafka를 통한 비동기 처리 확장성 확보
  • Redis를 통한 상태 유지 / 빠른 접근
  • MariaDB를 통한 영속적 저장
728x90
반응형
728x90
반응형

🎯 목표 시나리오 요약

  • 클라이언트 (임베디드 Windows 장비): WebSocket을 통해 서버에 데이터 전송
  • 서버 (Spring WebFlux 기반):
    • WebSocket endpoint 수신
    • 메시지를 Kafka로 발행 (Producer)
    • Redis에 일부 캐시/상태 저장 또는 Pub/Sub 처리
  • Kafka: 비동기 메시지 큐 / 로그 저장
  • Redis: 실시간 상태 관리 / 캐시 / Pub/Sub (optional)

🧱 전체 아키텍처 구조

┌─────────────────────────────────────┐
│       Embedded Device (Windows)    │
│  ┌─────────────┐                   │
│  │ WebSocket   │ ─────────┐        │
│  └─────────────┘          │        │
└───────────────────────────▼────────┘
                        WebSocket (Spring WebFlux)
                                │
                                ▼
                     ┌──────────────────────┐
                     │  WebSocketHandler    │
                     └──────────────────────┘
                                │
            ┌────────────┬──────────────┬────────────┐
            ▼            ▼              ▼            ▼
       Kafka Producer   Redis Cache   Validation    Logging
            │
            ▼
        Kafka Broker
            │
        (Optional Consumers)

🔧 서버 구성 기술 선택

컴포넌트기술
WebSocket 서버 Spring WebFlux (WebSocketHandler, ReactorNetty)
Kafka 연동 spring-kafka
Redis 연동 spring-data-redis 또는 lettuce
비동기 처리 Project Reactor (Flux, Mono) 기반 처리
 

✅ 주요 구성 요소 설명 및 구현 팁

1. WebSocket 서버 (Spring WebFlux)

@Component
public class DeviceWebSocketHandler implements WebSocketHandler {

    @Autowired
    private KafkaService kafkaService;

    @Autowired
    private RedisService redisService;

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        return session.receive()
            .map(WebSocketMessage::getPayloadAsText)
            .flatMap(payload -> {
                // 1. Redis에 상태 기록
                redisService.saveDeviceStatus(session.getId(), payload);

                // 2. Kafka로 전송
                return kafkaService.sendToKafka("device.data", payload);
            })
            .then();
    }
}

2. Kafka 전송 서비스

@Service
public class KafkaService {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    public Mono<Void> sendToKafka(String topic, String message) {
        return Mono.fromFuture(() ->
            kafkaTemplate.send(topic, message).completable()
        ).then();
    }
}

3. Redis 캐시 서비스

@Service
public class RedisService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public void saveDeviceStatus(String deviceId, String payload) {
        redisTemplate.opsForValue().set("device:" + deviceId, payload);
    }
}

4. WebSocket 라우팅 설정

@Configuration
public class WebSocketConfig {

    @Bean
    public HandlerMapping webSocketMapping(DeviceWebSocketHandler handler) {
        Map<String, WebSocketHandler> map = new HashMap<>();
        map.put("/ws/device", handler);

        SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
        mapping.setUrlMap(map);
        mapping.setOrder(10);
        return mapping;
    }

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
    }
}

💾 Redis 활용 방안

활용 방식 설명
장비 상태 저장 장비 상태나 마지막 수신 데이터를 Redis key-value로 캐싱
실시간 알림 Redis Pub/Sub으로 특정 이벤트 전파 (optional)
장애 복구 Kafka 데이터의 상태를 Redis로 보완 가능
 

🔄 Kafka 활용 방안

역할 설명
메시지 브로커 WebSocket에서 받은 데이터를 다른 서비스/분석 시스템으로 분기
로그 저장 모든 메시지를 저장하여 추후 분석 가능
분산 확장 다양한 Consumer 그룹에서 병렬 처리 가능
 

🧪 테스트 및 디버깅 팁

  • WebSocket 수신 로그에 session.getId() 로 장비별 식별자 유지
  • Kafka 전송은 .addCallback()으로 실패 대응
  • Redis TTL 설정을 활용해 오래된 상태 자동 제거
  • WebSocket 연결 수 제한, Ping/Pong 체크도 고려

🛡 보안 및 운영 고려사항

  • wss:// (TLS) 적용 (Spring WebFlux + Netty에서 SSL 설정)
  • WebSocket 인증 토큰 헤더 처리 (JWT 등)
  • Kafka 메시지 사이즈 제한 (max.request.size)
  • Redis 연결 풀 설정 (LettuceConnectionFactory)
  • 로그 및 traceId는 MDC로 분리 관리
728x90
반응형
728x90
반응형

Node.js 시스템을 임베디드 Windows 환경에 설치/구동하는 것이 Spring(Java)보다 더 복잡한가?
이 질문에 대한 답은 “요소별로 다르다” 입니다.
아래에 Node.js와 Spring(Java)의 설치·실행 비교임베디드 Windows 환경 기준으로 정리해드리겠습니다.


✅ 요약 결론

항목 Node.js Spring Boot
💾 초기 설치 난이도 간단함 (Node.js 설치만 있으면 끝) 중간 (JDK + 환경설정 필요)
🏗 빌드 & 패키징 가볍고 빠름 (JS 파일 + npm install) 무겁고 느림 (Gradle/Maven → fat JAR)
⚙ 실행 구조 node app.js 또는 npm start java -jar app.jar
📦 배포 단위 폴더 복사, script 포함 JAR 파일 1개 (내장 Tomcat 포함)
🧩 의존성 관리 node_modules 폴더로 제공 JAR에 모두 패키징 (spring-boot-maven-plugin)
🧠 OS 설치 경험 Node는 종종 비공식 스크립트로 복잡해짐 Java는 대체로 안정적 설치 지원
🧰 설치 후 유지보수 node_modules 깨짐 위험 ↑ Java는 안정성 높음
🛡 보안/복구성 단일 실행환경에 취약 JVM 기반이어서 복구 쉬움
 

📌 세부 비교

1. 설치 요구사항

항목 Node.js Spring Boot
런타임 설치 Node.js 설치 필요 (node, npm) JDK 설치 필요 (java, javac)
설치 크기 작음 (20~30MB) 큼 (JDK만 수백 MB)
환경 변수 설정 NODE_PATH 생략 가능 JAVA_HOME, PATH 설정 필요
임베디드 환경 적용 이식성 좋음 (폴더 복사로도 가능) 이식성 낮음 (JDK 설치가 까다로울 수 있음)
 

👉 Node.js는 런타임만 설치하면 실행이 간단, 반면 Java는 JDK 설정과 경로가 민감할 수 있음


2. 실행 방식

Node.js

node index.js
# 또는
npm start
  • package.json만 있다면 실행 명령어가 명확
  • 프로세스 매니저(pm2, nssm 등) 필요 시 외부 툴 사용

Java Spring Boot

java -jar your-app.jar
  • 단일 JAR 파일로 실행 가능 (Spring Boot의 장점)
  • 윈도우 서비스 등록 시 .bat 스크립트 또는 nssm 사용 가능

3. 배포 패턴

항목 Node.js Spring Boot
복사/설치 폴더 복사 후 npm install 또는 node_modules 포함 복사 단일 .jar 파일만 복사 가능
빌드 산출물 소스 파일 + node_modules Fat JAR 1개 (build/libs/*.jar)
윈도우 서비스 등록 nssm + node 실행 nssm + java -jar
 

4. 운영/유지보수

항목 Node.js Spring Boot
모듈 손상 시 복구 npm install 다시 필요 JAR 파일 하나로 복구 간단
로그 관리 수동 구현 많음 (winston, 파일 rotate 직접 처리) Spring logging.file, logback 내장
스케줄러, 백오프 등 별도 구현 필요 @Scheduled, RetryTemplate 등 내장
Redis/Firebase 연동 등 비공식 모듈 많음 (문서 부실 가능성) 공식 client 다양, 문서 풍부
 

💡 추천 판단 기준

상황 추천
빠르게 프로토타입 만들어야 함 Node.js
정식 설치 프로그램처럼 구성되어야 함 Java Spring Boot
향후 Redis, 분산락, FCM 등 고도화 필요 Java Spring Boot
운영자가 Java 익숙하지 않음 Node.js 가능
시스템이 하루 24시간 계속 떠 있어야 함 Java 안정성 우수
TCP 서버 등 스레드 처리 중요 Java (멀티스레드 유리)
 

🧩 실제 임베디드 Windows 배포 예시

Node.js

C:\MyApp\node.exe
C:\MyApp\app.js
C:\MyApp\package.json
C:\MyApp\node_modules\...

# nssm으로 등록
nssm install MyNodeApp "C:\MyApp\node.exe" "app.js"

Java Spring

C:\MyApp\java.exe
C:\MyApp\app.jar

nssm install MySpringApp "C:\MyApp\java.exe" "-jar app.jar"

✅ 결론

Node.js는 설치·배포가 간편하고 초기 진입이 쉬운 반면,
Java Spring은 장기 운영, 안정성, 확장성에서 우수합니다.

👉 임베디드 장비가 “정식 서비스 시스템”으로 운용될 예정이라면 → Java Spring이 더 적합

728x90
반응형
728x90
반응형

아주 좋은 질문입니다. Redisson 분산 락을 사용하고 있다면, "푸시 알림 상태(예: PENDING, IN_PROGRESS, RETRY, STUCK 등)를 세분화해야 하는가?" 라는 질문은 트랜잭션 보장, 중복 방지, 장애 복구 가능성 측면에서 판단해야 합니다.


✅ 결론 먼저

Redisson 분산 락이 제대로 적용되어 있고, 오직 하나의 인스턴스만 동시에 실행된다면, 상태값을 꼭 여러 개로 나눌 필요는 없습니다.
하지만 다음 조건 중 하나라도 해당된다면 상태값 분리는 매우 유의미합니다.


🔍 상태값을 나누는 이유는 다음과 같습니다:

1. 상태 기반 장애 진단 및 복구 처리 용이

  • IN_PROGRESS → 너무 오래되면 STUCK 으로 간주하여 자동 재시도
  • RETRY → retryCount와 함께 재시도 제어 가능

이 구조는 분산 환경에서 처리 중단/실패가 발생했을 때 복구 전략 수립이 매우 쉬워집니다.


2. 스케줄러 락이 실패하거나 강제 종료될 수 있음

Redisson 락이 있더라도:

  • 스케줄러가 시작됐지만 락을 못 잡은 경우: 다른 인스턴스가 처리해야 함
  • 스케줄러 도중 예외 발생 → 락은 풀렸지만 상태는 IN_PROGRESS로 남음
  • 스케줄러 강제 종료 (kill, crash 등) → STUCK 처리 필요

이런 경우 status 기반 복구 로직이 없으면 알림이 영원히 누락됩니다.


3. 재시도 정책을 세우기 위함

  • retryCount와 함께 RETRY 상태를 관리하면 N회 재시도 후 실패 상태로 전환 가능
  • 예를 들어 FCM 오류 코드에 따라 deviceToken을 폐기하거나 사용자에게 재등록 요청 가능

✳️ 만약 상태값을 단순화한다면?

PENDING → SUCCESS / FAILED

이 구조도 가능하지만 복구 시나리오가 단순해야 합니다:

  • 스케줄러는 반드시 락을 잡고 실행됨을 보장
  • 실패가 나면 단순 로그 남기고 무시
  • 중복 실행 가능성 없어야 함

단일 인스턴스 + 매우 안정적인 환경에서는 OK


💡 추천 전략 (Redisson 환경에서 이상적 구조)

상태 의미
PENDING 처리 대기 중
IN_PROGRESS 처리 중 (락 잡은 상태)
STUCK 일정 시간 이상 IN_PROGRESS 상태로 남은 경우
RETRY 실패 후 재시도 가능한 상태
SUCCESS 전송 성공
FAILED 재시도 불가/기한 만료
 

그리고 다음과 같은 전략:

  • 락 획득 시에만 IN_PROGRESS로 전환
  • 일정 시간 초과된 IN_PROGRESS는 STUCK으로 전환 후 다시 PENDING으로 되돌림
  • RETRY는 최대 retryCount 제한
  • 상태 전이는 트랜잭션 처리 또는 @Transactional 필요

🔧 실전에서 사용되는 상태 전이 흐름 예시

PENDING
  ↓ (락 획득)
IN_PROGRESS
  ↓ (성공)
SUCCESS
  ↓ (실패)
RETRY (→ 재시도 횟수 초과 시 → FAILED)
  ↓ (예외/Timeout)
STUCK (→ 수동 또는 주기적 재처리 대상)

✅ 정리

 

조건 상태값 분리 필요? 이유
분산 환경 ✅ 필요 충돌 방지 및 복구
장애 복구 필요 ✅ 필요 STUCK 처리 가능
재시도 필요 ✅ 필요 RETRY + retryCount
단일 인스턴스, 매우 단순 ❌ 생략 가능 락만으로 충분
728x90
반응형
728x90
반응형

💡 하드 리얼타임 시스템이란?

정해진 시간 내에 반드시 응답/처리를 완료해야 하는 시스템
즉, 딜레이 없이, 정해진 마감 시간 전에 처리를 못 하면 시스템이 실패로 간주되는 환경입니다.

✅ 조건 요약:

  • 응답 시간이 예측 가능하고 일정해야
  • JIT, GC 등 런타임 변동 요소 제거가 중요
  • "빠른 것"이 아니라 "일정하게 빠른 것" 이 중요

🧭 대표적인 도메인/시나리오 예시


도메인설명 하드 리얼타임  요구 이유
🛫 항공기 제어 시스템 (RTOS) 조종사 조작 입력 처리, 자동 조종 수 밀리초 이내에 응답 못 하면 안전 문제 발생
🚗 자동차 ECU (전장 제어) 브레이크 제어, 충돌 감지 → 에어백 10ms 이내에 작동해야 생명 보호
🏭 산업용 로봇/PLC 센서 감지 → 기계 동작 → 피드백 루프 주기적 제어 루프(1ms~10ms) 필수
🩺 의료 장비 (MRI, 심전도 측정기) 환자 상태 신호 수집 및 제어 측정 지연 = 진단 실패 가능성
🛰️ 군사 방어 체계 미사일 요격, 드론 통신 수 밀리초 오차도 치명적
🚦 실시간 교통 제어 시스템 교차로 신호 제어, 차량/보행자 감지 수십~수백 ms 단위 제어로 신호 안정 필요
 

⚠️ JVM 기반 시스템의 문제점


 

문제 설명
JIT 컴파일 시간 예측 불가 초기 Warm-up 시간 필요
GC Pause (Full GC 등) 수백 ms 정지 발생 가능
Thread scheduling 제어 어려움 OS 종속적이며 예상 시간 벗어남
Native와 달리 OS 타이머 및 IRQ 직접 처리 불가 RTOS 같은 환경에 적합하지 않음
 

✅ Native Image(GraalVM)가 적합한 이유


항목 설명
❌ JIT 제거 코드가 완전히 Ahead-of-Time 컴파일됨
❌ GC 예측 불가 → 해결 필요시 GC-less 설정도 가능 (NoGC)
✅ 실행 속도 + 일관성 평균 응답 속도뿐 아니라 분산도 적음
✅ Linux Real-Time Kernel과 직접 연계 바이너리로써 OS 수준 제어 용이
✅ Docker 없이도 단일 실행 가능 IoT/ECU 등 메모리 제한 환경에도 적합
 

🎯 예시 시나리오

🚗 자동차 전방 충돌 감지 시스템

  • 센서 데이터 수집 → 30ms 내에 판단 → 제동 여부 전송
  • 일반 JVM: GC pause 발생 시 200ms 이상 지연 가능성
  • Native Image: GC/Thread 오버헤드 제거 → 3~5ms 내에 안정 응답

🏭 공장 자동화 장비 (로봇 팔)

  • 일정한 주기(10ms)로 PLC 명령 수신 및 제어
  • Virtual Thread나 Event Loop 기반은 예측 어려움
  • Native Image는 고정된 실행 시간 확보

⚠️ 단, 주의할 점

  • Native Image는 JVM의 동적성, 유연성을 일부 포기
  • 리플렉션, 프록시, 동적 클래스 로딩 → 제한됨
  • Spring Boot AOT 및 GraalVM 설정 정교하게 필요

📌 결론


 

상황 선택
응답 지연이 실패로 직결되는 실시간 환경 ✅ Native Image 적극 추천
로직 복잡 + 일반 서버 환경 ❌ JVM 기반 운영 권장
임베디드 시스템, 전장 제어 등 ✅ Native + 경량화 필수
728x90
반응형
728x90
반응형

Spring Boot 3.x + Java 21 + GraalVM Native Image 조합은 최근 들어 성능과 배포 최적화를 위해 주목받고 있습니다.
단순히 빌드 방식만 바뀌는 것이 아니라 개발 관점과 배포 관점 모두에서 구조적 이점을 제공합니다.


🧱 Native Build란?

Java 애플리케이션을 JAR → JVM 실행이 아닌
시작 전 정적으로 컴파일된 네이티브 바이너리(.exe, ELF 등) 로 만드는 방식입니다.
GraalVM native-image 명령어로 생성되며, Spring Boot 3.1+는 공식 지원합니다.


🔧 개발 관점에서의 Native Build 고려 이유

 

항목 설명 장점
시작 속도 극대화 JVM 초기화 없이 바이너리 실행 <1초 기동 속도 (대부분 0.1~0.3초)
낮은 메모리 사용 사용되는 코드만 포함됨 (Dead Code 제거) RSS/Heap 메모리 30~80% 절감
보안성 향상 리플렉션, JAR 구조 없음 코드 난독화 수준으로 분석 어려움
JVM 의존성 제거 특정 Java 런타임 필요 없음 단독 실행 가능 (JRE 없는 서버에서도)
⚠️ 개발 진입장벽 있음 리플렉션, Proxy, Netty 등 등록 필요 Spring AOT가 도와줌 (@ReflectiveAccess)
Spring Boot 3.x 최적화 spring-aot, native-image 플러그인 제공 구조적으로 통합됨, GraalVM 설정 간소화
 

🚀 배포 관점에서의 Native Build 고려 이유


 

항목 설명 효과
🧊 Cold Start 대응 Serverless 환경, CLI 툴, 백오피스 등에서 효과 Lambda, Cloud Run 등에서 탁월
📦 이미지 용량 감소 JAR → 바이너리 → 컨테이너 슬림화 80MB → 20MB 수준까지 축소
🔐 JVM 제거 Java 설치 불필요, OS 바이너리만 필요 Alpine, distroless에 최적
성능 안정성 GC, JIT 변동 없음 → 예측 가능한 성능 하드 리얼타임 시스템에 적합
🐳 Kubernetes 최적화 빠른 Pod 시작 시간 + 리소스 절감 Scale-out 속도 향상
 

💥 실전 예시: WebFlux API를 Native로 빌드한 경우


항목 JVM JAR Native Image
기동 속도 2.5~4초 0.15초
메모리 사용 (idle 기준) 200~300MB 60~80MB
이미지 용량 (docker) 250MB 40MB
플랫폼 독립성 JVM 필요 JVM 필요 없음 (glibc/Linux만)
빌드 시간 수 초 수 분 (초기엔 3~10분)
 

✅ 어떤 상황에 Native Build를 고려해야 할까?

 

상황 Native Build 적합도
CLI 도구, 배치 실행, 단일 기능 API ✅ 매우 적합
AWS Lambda, Cloud Run, FaaS ✅ 강력 추천
리소스 제한 환경 (IoT, 모바일, 엣지) ✅ 매우 적합
고가용성 API 서버 (서비스 수천 개 배포) ⚠️ 초기엔 고려 가능, 아직 일부 제약
고속 처리 Kafka Consumer 등 ❌ 성능 손실 있을 수 있음 (JIT 없음)
 

🔧 Spring Boot Native 구성 요소

  • spring-aot 플러그인 (자동 최적화)
  • native-image 빌드 도구 (GraalVM 필요)
  • Docker 기반 Native 빌드 지원 (paketobuildpacks/builder:tiny)
  • Spring Cloud Gateway, WebClient, WebFlux 기본 지원

💡 결론 요약


관점 Native Build 도입 가치
개발 측면 빠른 시작, 메모리 절감, 보안 향상
배포 측면 Docker 이미지 최소화, JVM 제거, serverless 최적
권장 시나리오 CLI, Lambda, Micro Utility App, Mobile API
주의점 Reflection 등록 필요, 빌드 느림, 디버깅 어려움
728x90
반응형

+ Recent posts