[Java] RS-232 통신 구현하기: COM Port 검색·Baud Rate 설정·송수신 처리
Java로 RS-232 통신 구현하기: COM Port 검색·Baud Rate 설정·송수신 처리#
1장 Java 프로그램이 RS-232 장비와 통신하려면 무엇이 필요할까#
PC와 산업 장비가 RS-232로 연결되어 있다고 하겠습니다.
물리적으로는 다음과 같습니다.
Java Application
│
│ COM3
▼
Serial Port
│
│ RS-232
▼
Device하지만 Cable을 연결했다고 바로 통신되는 것은 아닙니다.
프로그램에서는 최소한 다음 조건이 맞아야 합니다.
올바른 COM Port
Baud Rate
Data Bits
Parity
Stop Bits
Flow Control
송수신 데이터 형식하나라도 틀리면 장비는:
응답 없음
깨진 문자
잘못된 Packet
Timeout처럼 보일 수 있습니다.
따라서 Java 장비 통신은 단순히 write() 한 번 호출하는 문제가 아닙니다.
Port를 찾고 → 통신 조건을 맞추고 → byte를 보내고 → 응답을 Buffer에 모아 → Protocol 단위로 해석하는 전체 과정을 구현해야 합니다.
2장 Java에서는 jSerialComm을 이용할 수 있다#
Java 기본 API만으로 모든 운영체제의 Serial Port를 동일한 방식으로 다루기에는 제약이 있기 때문에 일반적으로 별도의 Serial Communication Library를 사용합니다.
대표적인 선택지 중 하나가 jSerialComm입니다.
jSerialComm은 현재 다음과 같은 기능을 제공합니다.
Serial Port 검색
Baud Rate 설정
Data Bit 설정
Stop Bit 설정
Parity 설정
Flow Control
Read / Write Timeout
InputStream / OutputStream
Event Callback
장비 Disconnect 감지Windows, Linux, macOS 등 여러 Platform을 지원하도록 설계되어 있습니다.
기본 Import:
import com.fazecast.jSerialComm.SerialPort;실무에서는 Library Version을 프로젝트의 Maven 또는 Gradle Dependency로 명시해 관리하는 것이 좋습니다.
3장 첫 번째 단계는 COM Port를 찾는 것이다#
Windows에서는 Serial Port가 보통:
COM1
COM3
COM7같은 이름으로 나타납니다.
Linux에서는:
/dev/ttyS0
/dev/ttyUSB0
/dev/ttyACM0같은 Device Path를 만날 수 있습니다.
jSerialComm에서는 현재 시스템의 Serial Port를 나열할 수 있습니다. 포트 열거와 시스템 포트 이름·설명 조회 기능은 공식 기능으로 제공됩니다.
SerialPort[] ports = SerialPort.getCommPorts();
for (SerialPort port : ports) {
System.out.println(
port.getSystemPortName()
+ " / "
+ port.getDescriptivePortName()
);
}출력 예:
COM3 / USB Serial Port
COM5 / Communications Port
COM8 / USB-RS232 Adapter여기서 중요한 점은:
COM3이 존재한다.와:
COM3이 내가 원하는 장비다.는 다른 사실이라는 것입니다.
USB-RS232 Adapter를 여러 개 사용하는 환경에서는 COM 번호가 달라질 수 있으므로 Device Description, USB Port 위치, 장비 관리 정보 등을 함께 사용하는 것이 좋습니다.
4장 Serial Port 통신 조건을 정확하게 맞춘다#
장비 Manual에 다음과 같이 적혀 있다고 하겠습니다.
9600
8
N
1흔히:
9600-8-N-1이라고 표현합니다.
뜻은:
Baud Rate
9600
Data Bits
8
Parity
None
Stop Bits
1입니다.
Java에서는 다음처럼 설정할 수 있습니다.
SerialPort port = SerialPort.getCommPort("COM3");
port.setComPortParameters(
9600,
8,
SerialPort.ONE_STOP_BIT,
SerialPort.NO_PARITY
);jSerialComm은 Baud Rate, Data Bits, Stop Bits, Parity 및 Flow Control 구성을 지원합니다.
Baud Rate가 다르면 어떻게 될까#
장비:
9600 bpsJava:
19200 bps라면 정상적인 byte 해석이 어렵습니다.
결과는:
깨진 데이터
이상한 Hex 값
Checksum 오류
응답 없음등으로 나타날 수 있습니다.
그래서 Serial 장애에서는 가장 먼저:
Baud
Data
Parity
Stop
Flow Control을 장비 Manual과 비교해야 합니다.
5장 Port를 열고 데이터를 송신한다#
통신 Parameter 설정이 끝났다면 Port를 엽니다.
if (!port.openPort()) {
throw new IllegalStateException("Serial port open failed");
}그다음 OutputStream을 이용해 byte를 전송할 수 있습니다.
try {
var out = port.getOutputStream();
byte[] request = {
0x02,
0x10,
0x00,
0x03
};
out.write(request);
out.flush();
} catch (Exception e) {
e.printStackTrace();
}여기서 예제의 byte 배열은 실제 장비 명령이 아니라 흐름 설명을 위한 Test Frame입니다.
실제 장비에서는 반드시 제조사가 제공한 Protocol을 따라야 합니다.
예를 들어 실제 Packet이:
STX
Address
Command
Length
Data
Checksum
ETX구조라면 그 규칙에 맞춰 byte 배열을 만들어야 합니다.
6장 Serial 통신에서는 문자열보다 byte를 먼저 생각한다#
장비 통신을 구현할 때 흔한 실수가 있습니다.
out.write("OPEN".getBytes());를 보내면 장비가 이해할 것이라고 생각하는 것입니다.
장비 Protocol이 ASCII라면 가능할 수 있습니다.
하지만 Binary Protocol이라면 전혀 다릅니다.
예:
02 10 00 01 7A 03같은 Frame을 요구할 수 있습니다.
따라서 장비 Manual에서 먼저 확인해야 합니다.
ASCII Protocol인가?
Binary Protocol인가?
Command Code는 무엇인가?
Address가 있는가?
Length가 있는가?
CRC·Checksum이 있는가?
STX·ETX가 있는가?Java 내부에서는 가능한 한:
String이 아니라:
byte[]기준으로 통신 Layer를 설계하는 것이 안전합니다.
7장 데이터를 한 번 읽었다고 Packet 하나가 오는 것은 아니다#
Serial Communication에서 매우 중요한 부분입니다.
다음 Packet이 왔다고 하겠습니다.
02 10 04 41 42 43 44 9A 03Java의 한 번의 read()가 반드시 이 전체 Packet을 반환하는 것은 아닙니다.
첫 번째 Read:
02 10 04두 번째:
41 42세 번째:
43 44 9A 03처럼 나뉘어 들어올 수 있습니다.
반대로 여러 Packet이 한 Read에 함께 들어올 수도 있습니다.
PACKET 1 | PACKET 2따라서:
read() 한 번
=
Packet 한 개라고 구현하면 안 됩니다.
기본 구조는:
Serial Port
↓
Read
↓
Receive Buffer
↓
Packet Boundary 탐색
↓
Complete Packet 추출
↓
Parser가 되어야 합니다.
8장 Timeout을 반드시 설계한다#
장비에 Request를 보냈다고 하겠습니다.
Java
↓
Command
↓
Device정상이라면:
Device
↓
Response
↓
Java가 와야 합니다.
하지만 실제 현장에서는:
Cable 단선
장비 전원 OFF
잘못된 Baud Rate
Protocol 오류
장비 처리 지연등으로 Response가 오지 않을 수 있습니다.
따라서 프로그램이 무한정 기다려서는 안 됩니다.
jSerialComm은 Blocking·Non-blocking Read/Write Timeout 설정을 지원합니다.
예:
port.setComPortTimeouts(
SerialPort.TIMEOUT_READ_SEMI_BLOCKING,
1000,
0
);여기서는 Read Timeout을 1초로 두고 있습니다.
하지만:
1초가 모든 장비에 맞는 값은 아닙니다.
Timeout은:
장비 응답 시간
Protocol 규격
장비 부하
재시도 횟수
업무 중요도에 따라 결정해야 합니다.
9장 비동기 수신 구조를 만드는 것이 좋다#
장비가 언제 데이터를 보낼지 모르는 경우가 많습니다.
예:
Sensor Event
Alarm
Card Read
장비 상태 변경
주기적 Telemetry이런 경우 Main Thread에서 계속 Blocking Read를 하면 다른 작업에 영향을 줄 수 있습니다.
jSerialComm은 Stream 방식뿐 아니라 Event Callback 기반 데이터 수신도 지원합니다.
구조적으로는:
Serial Port
│
▼
Receive Event
│
▼
Buffer
│
▼
Packet Parser
│
▼
Business Logic처럼 역할을 분리하는 것이 좋습니다.
간단한 Listener 예:
port.addDataListener(new SerialPortDataListener() {
@Override
public int getListeningEvents() {
return SerialPort.LISTENING_EVENT_DATA_AVAILABLE;
}
@Override
public void serialEvent(SerialPortEvent event) {
if (event.getEventType()
!= SerialPort.LISTENING_EVENT_DATA_AVAILABLE) {
return;
}
byte[] data =
new byte[port.bytesAvailable()];
int read =
port.readBytes(data, data.length);
if (read > 0) {
handleReceivedBytes(data, read);
}
}
});실제 프로그램에서는 Callback 안에서 무거운 Business Logic을 오래 수행하기보다 Buffer나 Queue에 전달한 뒤 별도의 Worker에서 처리하는 구조를 고려할 수 있습니다.
10장 Buffer에서 Packet을 분리한다#
수신 Buffer:
02 10 03 41 42까지 들어왔다고 하겠습니다.
아직 Frame이 완성되지 않았을 수 있습니다.
조금 뒤:
43 7F 03이 들어옵니다.
전체:
02 10 03 41 42 43 7F 03이 되면 Packet을 완성할 수 있습니다.
Packet Boundary를 찾는 방식은 Protocol마다 다릅니다.
Delimiter 방식#
STX
...
ETXLength 방식#
Header
Length
Data
CRCFixed Length 방식#
항상 16 byteTiming 방식#
일정 시간의 Silence를 Frame 경계로 사용하는 Protocol도 있습니다.
그래서 Serial 통신 Library와 Packet Parser는 분리하는 것이 좋습니다.
Serial Library
→ byte를 받는다.
Packet Parser
→ byte 의미를 해석한다.11장 Hex Dump 로그는 장비 통신의 핵심 증거다#
다음 두 로그 중 어떤 것이 장애 분석에 더 유용할까요?
장비 응답 실패보다:
2026-09-24 18:30:10.124
PORT=COM3
9600-8-N-1
TX
02 10 00 01 13 03
2026-09-24 18:30:10.217
RX
02 06 00 01 A4 03가 훨씬 유용합니다.
장비 통신에서는 다음 정보를 함께 기록하는 것이 좋습니다.
Timestamp
Port
Baud Rate
TX / RX
Hex Dump
Packet Length
Timeout
Exception
Device ID
Correlation ID간단한 Hex 변환 함수:
static String toHex(byte[] data, int length) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(
String.format("%02X ", data[i] & 0xFF)
);
}
return sb.toString().trim();
}사용:
System.out.println(
"RX: " + toHex(data, read)
);이런 Raw Data Log가 있어야:
송신 자체가 안 됐는가?
응답이 안 왔는가?
응답은 왔지만 Parser가 틀렸는가?
CRC가 틀렸는가?를 구분할 수 있습니다.
12장 Port Open 실패도 여러 원인이 있다#
다음 Code가:
port.openPort()실패했다고 해서 Serial Device가 고장났다고 단정하면 안 됩니다.
가능한 원인은 다양합니다.
다른 프로그램이 Port 사용 중
COM Port 이름 오류
USB-RS232 Driver 문제
운영체제 권한 문제
USB Adapter Disconnect
Library·Architecture 문제특히 Windows에서는 Terminal Program이 COM Port를 이미 사용 중이면 Java Application이 열지 못할 수 있습니다.
Linux에서는 Device Permission도 확인해야 합니다.
예:
/dev/ttyUSB0에 현재 사용자 계정이 접근할 권한이 있는지 확인해야 합니다.
따라서 Error Log에는:
Port Name
OS
Library Version
Open 결과
Exception
Device Description등을 기록하는 것이 좋습니다.
13장 Reconnect를 무한 반복하면 안 된다#
USB-RS232 Adapter가 빠졌다고 하겠습니다.
프로그램이 즉시:
Reconnect
Reconnect
Reconnect
Reconnect
Reconnect를 반복하면 CPU·Log·Device Driver에 불필요한 부하를 줄 수 있습니다.
좀 더 안정적인 구조는:
Connection Lost
↓
Port Close
↓
대기
↓
Port 다시 탐색
↓
Open
↓
통신 Parameter 재설정
↓
Health Check
↓
정상 상태 복귀입니다.
필요하면:
1초
2초
4초
8초처럼 Retry 간격을 늘리는 Backoff도 사용할 수 있습니다.
그리고 반드시:
Port 재연결 성공과:
실제 Device Protocol 정상을 구분해야 합니다.
Port가 열렸다고 Device가 정상 응답한다는 뜻은 아닙니다.
14장 RS-232 프로그램의 구조를 분리하면 유지보수가 쉬워진다#
처음에는 모든 코드를 하나의 Class에 넣기 쉽습니다.
Port Open
Send
Receive
Packet Parse
Business Logic
DB 저장하지만 장비가 늘어나면 유지보수가 어렵습니다.
다음과 같이 분리하는 편이 좋습니다.
SerialPortManager
│
├─ Port 검색
├─ Open / Close
└─ Parameter 설정
SerialTransport
│
├─ Send
└─ Receive
ReceiveBuffer
│
└─ Raw Byte 누적
PacketParser
│
├─ Frame Boundary
├─ Length
└─ CRC
DeviceProtocol
│
├─ Command 생성
└─ Response 해석
DeviceService
│
└─ 실제 업무 처리이렇게 하면 나중에:
RS-232
→ RS-485
Serial
→ TCP로 Transport가 바뀌더라도 Protocol Layer를 재사용할 가능성이 높아집니다.
15장 간단한 통합 예제를 만들어보자#
다음은 Serial Port를 열고 Test Message를 보내고 응답을 읽는 흐름을 단순화한 예입니다.
import com.fazecast.jSerialComm.SerialPort;
import java.io.InputStream;
import java.io.OutputStream;
public class Rs232Example {
public static void main(String[] args) {
SerialPort port =
SerialPort.getCommPort("COM3");
port.setComPortParameters(
9600,
8,
SerialPort.ONE_STOP_BIT,
SerialPort.NO_PARITY
);
port.setComPortTimeouts(
SerialPort.TIMEOUT_READ_SEMI_BLOCKING,
1000,
0
);
if (!port.openPort()) {
System.err.println(
"Failed to open serial port"
);
return;
}
try (
InputStream in = port.getInputStream();
OutputStream out = port.getOutputStream()
) {
byte[] request = {
0x02,
0x10,
0x00,
0x03
};
out.write(request);
out.flush();
System.out.println(
"TX: " + toHex(
request,
request.length
)
);
byte[] buffer = new byte[256];
int length = in.read(buffer);
if (length > 0) {
System.out.println(
"RX: " + toHex(
buffer,
length
)
);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
port.closePort();
}
}
private static String toHex(
byte[] data,
int length
) {
StringBuilder sb =
new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(
String.format(
"%02X ",
data[i] & 0xFF
)
);
}
return sb.toString().trim();
}
}이 Code의 핵심은 특정 장비 Command가 아닙니다.
다음 흐름입니다.
COM Port 선택
↓
통신 Parameter 설정
↓
Port Open
↓
Request byte 생성
↓
전송
↓
응답 수신
↓
Hex Log
↓
Port Close실제 Application에서는 여기에 Packet Parser와 비동기 처리, Retry와 상태 관리가 추가됩니다.
16장 장애를 만났을 때 어떤 순서로 볼까#
Java 프로그램이 장비와 통신하지 못한다고 하겠습니다.
다음 순서로 확인하면 좋습니다.
1단계 물리 연결#
Cable
TX / RX
GND
DTE / DCE
USB-RS232 Adapter
Device Power2단계 Operating System#
COM Port 존재
Driver
Port Permission
다른 Process의 Port 점유3단계 Serial 설정#
Baud Rate
Data Bits
Parity
Stop Bits
Flow Control4단계 송신#
실제로 byte가 나갔는가?
TX Hex가 맞는가?5단계 수신#
응답 byte가 들어오는가?
Read Timeout인가?6단계 Protocol#
STX / ETX
Length
Address
Command
CRC / Checksum이렇게 경계를 나누면:
통신이 안 됩니다.라는 막연한 문제를 구체적인 원인으로 좁힐 수 있습니다.
17장 흔히 하는 잘못된 구현#
read() 한 번을 Packet 하나라고 생각한다#
Serial Port는 Application Packet 경계를 자동으로 보장하지 않습니다.
Buffer와 Parser가 필요합니다.
수신 byte를 바로 String으로 변환한다#
Binary Protocol이라면 데이터가 깨지거나 의미를 잃을 수 있습니다.
먼저 byte와 Hex 기준으로 확인합니다.
Timeout을 두지 않는다#
장비 응답이 없을 때 Thread가 오래 대기하거나 Application 흐름이 멈출 수 있습니다.
Exception을 무시한다#
현장 장애의 중요한 단서가 사라집니다.
Port를 닫지 않는다#
재실행 때 Port 점유 문제로 이어질 수 있습니다.
UI Thread에서 Serial Read를 직접 한다#
응답이 늦으면 UI가 멈출 수 있습니다.
Port Open 성공을 장비 정상으로 판단한다#
Port Open과 Device Protocol 응답은 서로 다른 상태입니다.
18장 자기 점검#
Java에서 Serial Port를 어떻게 찾을 수 있는가#
jSerialComm의 SerialPort.getCommPorts()를 이용해 현재 시스템에서 인식된 Serial Port 목록을 확인할 수 있습니다.
9600-8-N-1은 무엇을 의미하는가#
Baud Rate 9600, Data Bits 8, Parity None, Stop Bits 1을 의미합니다.
왜 read() 한 번으로 Packet 하나를 처리하면 안 되는가#
Serial Stream의 Read 단위와 Application Protocol의 Packet 경계가 일치한다는 보장이 없기 때문입니다.
Port는 열렸는데 장비 응답이 없다면 무엇을 확인해야 하는가#
TX·RX 결선, Baud·Parity·Stop Bit, Flow Control, 송신 Frame, Device Address와 Protocol 규격을 순서대로 확인해야 합니다.
Hex Dump를 남기는 이유는 무엇인가#
실제 송수신된 Raw Byte를 확인해 물리 통신 문제와 Protocol Parser 문제를 분리하기 위해서입니다.
19장 이 글을 마치며#
Java에서 RS-232 장비와 통신하는 기본 흐름은 다음과 같습니다.
COM Port 검색
↓
장비 Port 식별
↓
Baud·Data·Parity·Stop 설정
↓
Port Open
↓
byte 송신
↓
byte 수신
↓
Receive Buffer
↓
Packet Parser
↓
업무 처리특히 다음 다섯 가지를 기억하면 됩니다.
RS-232 프로그램에서는 COM Port를 여는 것보다 장비와 동일한 Baud Rate·Data Bits·Parity·Stop Bits·Flow Control을 정확하게 맞추는 것이 먼저입니다.
장비 통신 데이터는 가능한 한 String보다 Raw byte 기준으로 확인하고 Hex Dump를 남겨야 장애 원인을 정확히 추적할 수 있습니다.
Serial read()의 반환 단위와 장비 Protocol의 Packet 경계는 다를 수 있으므로 Receive Buffer와 Packet Parser를 별도로 설계해야 합니다.
Port Open 성공·byte 수신 성공·Protocol 정상 응답·실제 업무 완료는 각각 다른 상태이므로 하나의 성공 여부로 합쳐 판단하면 안 됩니다.
Port 검색·Transport·Buffer·Packet Parser·Device Protocol·Business Logic을 분리해두면 장비 종류가 늘어나거나 RS-232에서 RS-485·TCP로 확장할 때 유지보수가 훨씬 쉬워집니다.
결국 Java로 RS-232 통신을 구현한다는 것은 Serial Port에 byte를 쓰고 읽는 것만을 의미하지 않습니다.
운영체제의 COM Port에서 시작해 실제 Wire를 지나 들어온 byte를 Buffer에 안전하게 모으고, 장비 Protocol의 Packet으로 복원한 뒤 Application이 신뢰할 수 있는 데이터로 전달하는 전체 통신 경로를 구현하는 것이 핵심입니다.