<scshin />
👋 환영합니다

생각을 기록하고
경험을 공유합니다

기술, 일상, 그리고 다양한 생각들을 담은 개인 블로그입니다. 함께 성장하고 배워가는 공간이 되길 바랍니다.

10게시글
3카테고리
0태그
[KVM/libvirt] Oracle Linux VM 디스크 용량 확장하기카테고리

[KVM/libvirt] Oracle Linux VM 디스크 용량 확장하기

[KVM/libvirt] Oracle Linux VM 디스크 용량 확장하기 — qcow2 백업부터 LVM·XFS 확장까지 가상머신을 운영하다 보면 로그나 데이터가 쌓이면서 처음 할당한 디스크 공간이 부족해질 수 있습니다. 이때 호스트에서 qcow2 파일의 용량만 늘리면 작업이 끝날 것 같지만, VM 내부에서도 추가 작업이 필요합니다. 가상 디스크를 늘리는 것과 운영체제가 실제로 사용할 공간을 늘리는 것은 서로 다른 작업이기 때문입니다. 이 글에서는 Oracle Linux VM의 디스크를 100GiB에서 200GiB로 확장하는 예시를 기준으로, 작업 전 백업부터 확장, 문제 발생 시 복구하는 방법까지 정리합니다. *** 1. 전체 구조 이해하기 이번 작업은 크게 호스트 작업과 VM 내부 작업으로 나뉩니다. Oracle Linux에서 LVM을 사용하는 경우 보통 다음과 같은 구조입니다. 호스트의 qcow2 파일 │ ▼ VM의 가상 디스크 /dev/vda │ ▼ 디스크 파티션 /dev/vda2 │ ▼ LVM Physical Volume /dev/vda2 │ ▼ LVM Volume Group ol │ ▼ LVM Logical Volume /dev/ol/root │ ▼ XFS 파일시스템 / 즉, 바깥쪽 디스크 공간을 늘린 뒤 내부에서도 순서대로 공간을 확장해야 합니다. 전체 작업 흐름은 다음과 같습니다. 현재 구성 확인 ↓ VM 정상 종료 ↓ VM 설정 및 qcow2 백업 ↓ 백업 검사 ↓ qcow2 확장 ↓ VM 시작 ↓ 파티션 확장 ↓ PV 확장 ↓ LV 확장 ↓ XFS 확장 ↓ 최종 확인 *** 2. 예시 환경 이 글에서는 다음 환경을 기준으로 설명합니다. *** 3. VM 내부에서 현재 디스크 구조 확인 먼저 Oracle Linux VM에 접속하여 현재 구성을 확인합니다. lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT 파일시스템 확인: df -Th / LVM 상태 확인: pvs vgs lvs -o lv_path,lv_size,vg_name 예를 들어 다음과 같은 구조라고 가정합니다. NAME SIZE TYPE FSTYPE MOUNTPOINT vda 100G disk ├─vda1 1G part xfs /boot └─vda2 99G part LVM2_member ├─ol-root 90G lvm xfs / └─ol-swap 9G lvm swap [SWAP] 이 경우 확장 대상은 다음과 같습니다. /dev/vda ↓ /dev/vda2 ↓ /dev/ol/root ↓ / *** 4. 호스트에서 VM 디스크 확인 이제 Oracle Linux VM이 아니라 libvirt 호스트 서버에서 작업합니다. VM 목록 확인: virsh list --all 대상 VM의 디스크 경로 확인: virsh domblklist oraclelinux --details 예: Type Device Target Source -------------------------------------------------------------- file disk vda /var/lib/libvirt/images/oraclelinux.qcow2 여기서 "Source"가 실제 qcow2 파일 경로입니다. *** 5. 작업 변수 설정 명령어를 편하게 사용하기 위해 변수를 지정합니다. VM="oraclelinux" DISK="/var/lib/libvirt/images/oraclelinux.qcow2" STAMP="$(date +%Y%m%d_%H%M%S)" BACKUP_DIR="/backup/libvirt/${VM}_${STAMP}" mkdir -p -m 700 "$BACKUP_DIR" 생성된 백업 경로 확인: echo "$BACKUP_DIR" 예: /backup/libvirt/oraclelinux_20260907_153500 *** 6. 백업 공간 확인 원본 디스크와 백업 위치의 여유 공간을 먼저 확인합니다. df -h "$(dirname "$DISK")" "$BACKUP_DIR" qcow2 파일 실제 사용량 확인: du -h "$DISK" qcow2는 sparse 파일이므로 "ls -lh"로 보이는 크기와 실제 디스크 사용량이 다를 수 있습니다. 예: virtual size : 100G 실제 사용량 : 28G «"/backup" 디렉터리가 원본 qcow2와 같은 물리 디스크에 존재한다면 해당 디스크 자체가 고장났을 경우 원본과 백업을 동시에 잃을 수 있습니다. 중요한 운영 서버라면 별도의 디스크나 NAS 등에 추가 백업하는 것을 권장합니다.» *** 7. VM 정상 종료 VM을 종료합니다. virsh shutdown "$VM" 상태 확인: virsh domstate "$VM" 다음과 같이 나와야 합니다. shut off 또는: virsh list --all Id Name State ----------------------------- - oraclelinux shut off «"virsh shutdown" 명령을 실행했다고 해서 즉시 종료된 것은 아닙니다. 반드시 "shut off" 상태를 확인한 후 다음 작업을 진행합니다.» VM이 종료되지 않을 경우 최후의 수단으로: virsh destroy "$VM" 를 사용할 수 있지만, 이는 강제 전원 종료이므로 가급적 사용하지 않는 것이 좋습니다. *** 8. qcow2 정보 확인 VM이 완전히 종료된 상태에서 확인합니다. qemu-img info --backing-chain "$DISK" 예: image: /var/lib/libvirt/images/oraclelinux.qcow2 file format: qcow2 virtual size: 100 GiB disk size: 28 GiB 여기서 중요한 값은 다음과 같습니다. * "virtual size": VM에서 보이는 가상 디스크 크기 * "disk size": 호스트에서 qcow2가 실제 사용하는 공간 만약 "backing file"이 존재한다면 현재 qcow2 파일만 복사해서는 완전한 백업이 되지 않을 수 있으므로 주의해야 합니다. *** 9. VM 설정 XML 백업 VM 설정도 같이 백업합니다. virsh dumpxml --inactive "$VM" > "$BACKUP_DIR/domain.xml" 백업 확인: ls -lh "$BACKUP_DIR" *** 10. qcow2 데이터 백업 VM이 종료된 상태에서 qcow2 파일을 복사합니다. cp -a --sparse=always --reflink=never \ "$DISK" \ "$BACKUP_DIR/disk.qcow2" 옵션 의미: 백업 파일 확인: ls -lh "$BACKUP_DIR" 실제 사용량: du -sh "$BACKUP_DIR/disk.qcow2" *** 11. qcow2 백업 검사 백업 qcow2 구조 검사: qemu-img check -f qcow2 "$BACKUP_DIR/disk.qcow2" 정상이면 다음과 비슷하게 표시됩니다. No errors were found on the image. 원본과 백업의 디스크 내용 비교: qemu-img compare -p -f qcow2 -F qcow2 \ "$DISK" \ "$BACKUP_DIR/disk.qcow2" 정상이면: Images are identical. 라고 표시됩니다. «"qemu-img check"는 qcow2 이미지 구조를 검사하는 것이며, Oracle DB나 애플리케이션의 논리적인 데이터 정상 여부까지 보장하는 것은 아닙니다.» *** 12. qcow2 용량 확장 이제 실제 가상 디스크를 확장합니다. VM이 여전히 꺼져 있는지 다시 확인합니다. virsh domstate "$VM" "shut off" 상태라면 진행합니다. 100GiB에서 200GiB로 변경: qemu-img resize -f qcow2 "$DISK" 200G 여기서 "200G"는 최종 용량입니다. 기존 용량에서 100GiB를 추가하려면 다음처럼 사용할 수도 있습니다. qemu-img resize -f qcow2 "$DISK" +100G 확인: qemu-img info "$DISK" 예: virtual size: 200 GiB 이미지 검사: qemu-img check -f qcow2 "$DISK" *** 13. VM 시작 호스트에서 VM을 다시 시작합니다. virsh start "$VM" 상태 확인: virsh list *** 14. Oracle Linux에서 디스크 증가 확인 이제 Oracle Linux VM 내부로 다시 접속합니다. lsblk 예: NAME SIZE TYPE MOUNTPOINT vda 200G disk ├─vda1 1G part /boot └─vda2 99G part ├─ol-root 90G lvm / └─ol-swap 9G lvm [SWAP] 여기서 중요한 부분은: vda = 200G vda2 = 99G 입니다. 즉, qcow2와 가상 디스크는 증가했지만 파티션은 아직 기존 크기입니다. *** 15. growpart 설치 Oracle Linux 8/9 기준: dnf install -y cloud-utils-growpart lvm2 xfsprogs Oracle Linux 7이라면: yum install -y cloud-utils-growpart lvm2 xfsprogs 설치 확인: which growpart *** 16. 파티션 확장 이 예시에서는 LVM 파티션이 "/dev/vda2"입니다. 먼저 dry-run으로 확인합니다. growpart -N /dev/vda 2 문제가 없다면 실제 확장합니다. growpart /dev/vda 2 여기서 의미는: /dev/vda = 대상 디스크 2 = 두 번째 파티션 입니다. 따라서 다음과 같이 사용하면 안 됩니다. growpart /dev/vda2 올바른 명령은: growpart /dev/vda 2 입니다. 확인: lsblk 예: vda 200G ├─vda1 1G └─vda2 199G *** 17. LVM PV 확장 파티션이 커졌으므로 LVM의 Physical Volume도 확장해야 합니다. pvresize /dev/vda2 확인: pvs vgs 예: VG #PV #LV #SN Attr VSize VFree ol 1 2 0 wz--n- <199.00g 100.00g 여기서 "VFree"가 증가했다면 정상입니다. «기존 PV에는 "pvcreate"가 아니라 "pvresize"를 사용해야 합니다.» *** 18. Logical Volume 확장 현재 LV 경로 확인: lvs -o lv_path,lv_size,vg_name 예: LV Path LV Size /dev/ol/root 90.00g /dev/ol/swap 9.00g VG의 남은 공간을 전부 "/"에 할당: lvextend -l +100%FREE /dev/ol/root 확인: lvs «"+100%FREE"는 VG에 남아 있는 공간을 전부 해당 LV에 할당한다는 의미입니다. 다른 LV에도 공간을 사용할 계획이라면 필요한 크기만 지정해야 합니다.» 예를 들어 50GiB만 추가하려면: lvextend -L +50G /dev/ol/root *** 19. XFS 파일시스템 확장 Logical Volume을 늘렸다고 실제 파일시스템 크기가 자동으로 늘어나는 것은 아닙니다. 먼저 파일시스템 확인: df -Th / 예: Filesystem Type Size Used Avail Use% Mounted on /dev/mapper/ol-root xfs 90G 25G 65G 28% / "Type"이 "xfs"이면 다음을 실행합니다. xfs_growfs / «명령어는 "xfs_growfs" 입니다. 아래처럼 입력하면 오타입니다. xfs_growfx / 이 경우: bash: xfs_growfx: 명령을 찾을 수 없습니다 와 같은 메시지가 나옵니다.» 만약 "xfs_growfs" 명령 자체가 없다면: dnf install -y xfsprogs 설치 후 다시 실행합니다. xfs_growfs / *** 20. 최종 확인 마지막으로 전체 구조를 확인합니다. lsblk 파일시스템 확인: df -Th / LVM 확인: pvs vgs lvs 예: Filesystem Type Size Used Avail Use% Mounted on /dev/mapper/ol-root xfs 190G 25G 165G 14% / 이렇게 "/" 용량이 증가했다면 작업이 완료된 것입니다. *** 21. 간단한 명령어 요약 호스트 VM="oraclelinux" DISK="/var/lib/libvirt/images/oraclelinux.qcow2" STAMP="$(date +%Y%m%d_%H%M%S)" BACKUP_DIR="/backup/libvirt/${VM}_${STAMP}" mkdir -p -m 700 "$BACKUP_DIR" virsh shutdown "$VM" virsh domstate "$VM" virsh dumpxml --inactive "$VM" > "$BACKUP_DIR/domain.xml" cp -a --sparse=always --reflink=never \ "$DISK" \ "$BACKUP_DIR/disk.qcow2" qemu-img check -f qcow2 "$BACKUP_DIR/disk.qcow2" qemu-img compare -p -f qcow2 -F qcow2 \ "$DISK" \ "$BACKUP_DIR/disk.qcow2" qemu-img resize -f qcow2 "$DISK" 200G qemu-img info "$DISK" virsh start "$VM" Oracle Linux VM 내부 dnf install -y cloud-utils-growpart lvm2 xfsprogs lsblk growpart -N /dev/vda 2 growpart /dev/vda 2 pvresize /dev/vda2 vgs lvextend -l +100%FREE /dev/ol/root xfs_growfs / df -Th / *** 22. LV와 XFS를 한 번에 확장하는 방법 "lvextend"의 "-r" 옵션을 이용하면 LV와 파일시스템을 함께 확장할 수도 있습니다. lvextend -r -l +100%FREE /dev/ol/root 따라서 다음처럼 사용할 수 있습니다. growpart /dev/vda 2 pvresize /dev/vda2 lvextend -r -l +100%FREE /dev/ol/root 마지막 확인: df -h / 다만 처음 작업하는 경우에는: 파티션 → PV → LV → XFS 각 단계가 어떻게 동작하는지 이해하기 위해 개별 명령으로 실행하는 편이 이해하기 쉽습니다. *** 23. 백업으로 복구하는 방법 용량 확장 과정에서 문제가 생겼다면 작업 전 백업한 qcow2 파일로 복구할 수 있습니다. «백업을 복원하면 VM 디스크 데이터가 백업 당시 시점으로 돌아갑니다.» 호스트에서 작업합니다. 변수 설정: VM="oraclelinux" DISK="/var/lib/libvirt/images/oraclelinux.qcow2" BACKUP_DIR="/backup/libvirt/oraclelinux_20260907_153500" 백업 확인: ls -lh "$BACKUP_DIR/domain.xml" "$BACKUP_DIR/disk.qcow2" VM 종료: virsh shutdown "$VM" 상태 확인: virsh domstate "$VM" 반드시: shut off 상태인지 확인합니다. 백업 검사: qemu-img check -f qcow2 "$BACKUP_DIR/disk.qcow2" 현재 디스크를 삭제하지 않고 별도 이름으로 보존합니다. FAILED_DISK="${DISK}.before_restore_$(date +%Y%m%d_%H%M%S)" mv "$DISK" "$FAILED_DISK" 백업 복원: cp -a --sparse=always --reflink=never \ "$BACKUP_DIR/disk.qcow2" \ "$DISK" 기존 파일 기준 소유권과 권한 복구: chown --reference="$FAILED_DISK" "$DISK" chmod --reference="$FAILED_DISK" "$DISK" SELinux를 사용하는 호스트라면: restorecon -v "$DISK" 확인: ls -lZ "$DISK" 이미지 검사: qemu-img check -f qcow2 "$DISK" VM 시작: virsh start "$VM" ***

September 7, 2026
Spring Integration이란? 개념부터 데이터 수집 시스템 적용까지카테고리

Spring Integration이란? 개념부터 데이터 수집 시스템 적용까지

Spring Integration이란? 개념부터 데이터 수집 시스템 적용까지 Spring 기반 애플리케이션을 개발하다 보면 단순한 CRUD를 넘어 여러 시스템과 데이터를 주고받아야 하는 경우가 있습니다. 예를 들어 데이터 수집 시스템에서는 다음과 같은 처리가 필요할 수 있습니다. 센서 / 장비 / 외부 API ↓ 데이터 수신 ↓ JSON 파싱 ↓ 데이터 검증 ↓ DB 저장 ↓ 데이터 변환/매핑 ↓ 데이터허브 전송 단순한 시스템이라면 각각의 기능을 Service에서 순차적으로 호출해도 충분합니다. 하지만 수집 대상과 프로토콜이 많아지고 오류 처리, 분기, 재처리 등의 요구사항이 추가되면 코드가 빠르게 복잡해집니다. 이러한 **시스템 간 데이터 흐름과 메시지 처리를 체계적으로 구현하기 위한 Spring 프레임워크가 "Spring Integration"**입니다. *** 1. Spring Integration이란? Spring Integration은 Spring Framework 기반의 EAI(Enterprise Application Integration) 프레임워크입니다. 쉽게 표현하면, «여러 시스템에서 들어오는 데이터를 메시지 형태로 받아 변환, 검증, 분기, 저장, 전송하는 흐름을 구성하는 프레임워크» 라고 할 수 있습니다. Spring Integration은 메시징 기반으로 동작하며 다음과 같은 다양한 시스템과 연동할 수 있습니다. * HTTP * TCP * UDP * MQTT * Kafka * JMS * FTP * SFTP * File * Database * Mail 예를 들어 IoT 장비에서 TCP로 데이터를 받고 이를 JSON으로 변환하여 Oracle DB에 저장한 뒤 데이터허브 API로 전달하는 구조를 만들 수 있습니다. IoT 장비 ↓ TCP ↓ Spring Integration ↓ JSON Parsing ↓ Validation ↓ Oracle ↓ HTTP ↓ DataHub *** 2. Spring Integration의 핵심 개념 Spring Integration을 이해하려면 다음 개념을 먼저 알아두는 것이 좋습니다. Message Channel Endpoint Adapter Gateway Transformer Filter Router Service Activator Integration Flow Error Channel 하나씩 살펴보겠습니다. *** 3. Message Spring Integration에서 데이터 처리의 기본 단위는 "Message"입니다. Message는 크게 두 부분으로 구성됩니다. Message ├── Payload └── Headers Payload 실제로 처리할 데이터입니다. 예를 들어 장비에서 다음 JSON이 들어왔다고 가정하겠습니다. { "deviceId": "DEV001", "temperature": 25.4, "humidity": 62 } 이 JSON 문자열 또는 이를 변환한 객체가 Payload가 됩니다. Headers 데이터 처리에 필요한 부가정보입니다. 예를 들면 다음과 같습니다. sourceIp protocol deviceId receivedAt serviceType 개념적으로 다음과 같은 Message가 만들어질 수 있습니다. Message ├── Payload │ └── { │ "deviceId": "DEV001", │ "temperature": 25.4 │ } │ └── Headers ├── sourceIp = 192.168.0.10 ├── protocol = TCP └── receivedAt = 2026-08-12 13:30:00 *** 4. Message Channel "Message Channel"은 Message가 이동하는 통로입니다. Producer ↓ Channel ↓ Consumer Spring Integration에서는 처리 단계 사이를 Channel로 연결합니다. 데이터 수신 ↓ receiveChannel ↓ JSON Parsing ↓ parseChannel ↓ DB 저장 대표적인 Channel 종류는 다음과 같습니다. DirectChannel 가장 기본적인 Channel입니다. 메시지를 보내면 동일한 처리 흐름에서 다음 Handler가 즉시 실행됩니다. @Bean public MessageChannel receiveChannel() { return new DirectChannel(); } 구조는 단순합니다. A → B 대부분의 기본 Integration Flow에서는 "DirectChannel"부터 사용하면 됩니다. *** QueueChannel 메시지를 Queue에 저장한 후 Consumer가 가져가는 방식입니다. Producer ↓ Queue ↓ Consumer 수집 속도와 처리 속도가 다르거나 일시적으로 메시지를 보관해야 할 때 사용할 수 있습니다. *** PublishSubscribeChannel 하나의 메시지를 여러 Consumer에게 전달할 때 사용합니다. → DB 저장 Message ────→ 로그 저장 → 모니터링 하나의 수신 데이터를 여러 처리기에 동시에 전달해야 할 때 유용합니다. *** 5. Integration Flow "IntegrationFlow"는 Spring Integration의 핵심입니다. 데이터가 어떤 순서로 처리될지를 정의합니다. 예를 들어 다음과 같은 수집 프로세스가 있다고 가정합니다. 수신 ↓ JSON Parsing ↓ Validation ↓ DB 저장 ↓ DataHub 전송 Java DSL을 사용하면 다음처럼 표현할 수 있습니다. @Bean public IntegrationFlow collectFlow() { return IntegrationFlow .from("receiveChannel") .handle(jsonParseService, "parse") .handle(validationService, "validate") .handle(collectService, "save") .handle(dataHubService, "send") .get(); } 일반적인 Spring 코드라면 다음과 비슷합니다. public void collect(String data) { Object parsedData = jsonParseService.parse(data); validationService.validate(parsedData); collectService.save(parsedData); dataHubService.send(parsedData); } 두 방식의 차이는 처리 흐름 자체를 코드에서 직접 관리하느냐, Integration Flow가 관리하느냐에 있습니다. *** 6. Inbound Adapter와 Outbound Adapter Spring Integration에서 외부 시스템과 데이터를 주고받는 역할을 Adapter가 담당합니다. Inbound Adapter 외부 시스템에서 Spring 애플리케이션으로 데이터가 들어오는 부분입니다. 외부 시스템 ↓ Inbound Adapter ↓ Spring Integration 예를 들면 다음과 같습니다. TCP 장비 → Spring HTTP API → Spring MQTT → Spring File → Spring *** Outbound Adapter Spring에서 외부 시스템으로 데이터를 보내는 역할입니다. Spring Integration ↓ Outbound Adapter ↓ 외부 시스템 예: Spring → HTTP API Spring → Kafka Spring → MQTT Spring → TCP Server 따라서 데이터 수집 시스템에서는 다음과 같은 구성이 가능합니다. 센서 ↓ TCP Inbound Adapter ↓ Spring Integration ↓ Oracle ↓ HTTP Outbound Adapter ↓ 데이터허브 *** 7. Transformer Transformer는 데이터를 다른 형태로 변환할 때 사용합니다. 장비마다 보내는 데이터 규격이 다를 경우 특히 유용합니다. 예를 들어 장비가 다음과 같이 데이터를 전송한다고 가정합니다. { "tm": "25.3", "hm": "62" } 하지만 내부 시스템에서는 다음 형식을 사용한다고 하겠습니다. { "temperature": 25.3, "humidity": 62 } 이때 Transformer를 이용하여 다음과 같이 변환합니다. 장비 원본 데이터 ↓ Transformer ↓ 내부 표준 데이터 Java DSL에서는 다음과 같이 사용할 수 있습니다. .transform(message -> { return convertData(message); }) 또는 별도의 Service로 구현할 수도 있습니다. .handle(dataTransformService, "transform") 실제 프로젝트에서는 변환 로직이 복잡해질 가능성이 높기 때문에 별도의 Service 클래스로 분리하는 방식이 관리하기 편합니다. *** 8. Filter Filter는 조건에 맞지 않는 메시지를 걸러냅니다. 예를 들어 온도 데이터가 허용 가능한 범위인지 확인할 수 있습니다. .filter(data -> data.getTemperature() >= -50 && data.getTemperature() <= 100 ) 처리 흐름은 다음과 같습니다. 수신 데이터 ↓ Filter ↙ ↘ 정상 비정상 ↓ ↓ 처리 오류 처리 다음과 같은 검증에 사용할 수 있습니다. * 필수 데이터 존재 여부 * 값 범위 검증 * 장비 등록 여부 * 서비스 활성화 여부 * 데이터 중복 여부 *** 9. Router Router는 메시지의 조건에 따라 처리 흐름을 나누는 역할을 합니다. 예를 들어 하나의 수집 서버에서 여러 종류의 서비스를 처리한다고 가정해보겠습니다. 환경센서 CCTV 교통센서 스마트부이 수신된 서비스 종류에 따라 다음과 같이 분기할 수 있습니다. Message ↓ Router ┌─────────┼─────────┐ ↓ ↓ ↓ 환경센서 CCTV 교통 ↓ ↓ ↓ Env Flow CCTV Flow Traffic Flow 예를 들면 다음과 같이 구성할 수 있습니다. .route( CollectData::getServiceType, mapping -> mapping .subFlowMapping("ENV", environmentFlow()) .subFlowMapping("CCTV", cctvFlow()) .subFlowMapping("TRAFFIC", trafficFlow()) ) 여러 종류의 서비스를 하나의 수집 서버에서 처리할 때 매우 유용한 기능입니다. *** 10. Service Activator Service Activator는 메시지를 실제 Spring Service에 전달하여 업무 로직을 실행합니다. 예를 들어 수집 데이터를 DB에 저장하는 Service가 있다고 가정합니다. @Service public class CollectService { public CollectData save(CollectData data) { // DB 저장 return data; } } Integration Flow에서는 다음과 같이 호출할 수 있습니다. .handle(collectService, "save") 즉 Spring Integration을 사용한다고 해서 기존 Spring의 Service나 MyBatis 구조를 버리는 것은 아닙니다. 오히려 다음과 같이 역할을 분리하는 것이 좋습니다. Spring Integration ↓ 데이터 흐름 제어 Service ↓ 업무 로직 Mapper / MyBatis ↓ Database *** 11. Error Channel 데이터 수집 시스템에서는 오류 처리가 매우 중요합니다. Spring Integration에서는 처리 중 발생한 오류를 "errorChannel"로 전달할 수 있습니다. 데이터 수신 ↓ JSON Parsing ↓ ERROR ↓ errorChannel ↓ 오류 이력 저장 다음과 같이 별도의 Error Flow를 만들 수 있습니다. @Bean public IntegrationFlow errorFlow() { return IntegrationFlow .from("errorChannel") .handle(errorService, "handle") .get(); } 예를 들어 다음과 같은 오류코드를 정의할 수 있습니다. COL-1001 CONNECTION_FAILED COL-1002 CONNECTION_TIMEOUT COL-2001 EMPTY_DATA COL-2003 JSON_PARSE_ERROR COL-2005 REQUIRED_FIELD_MISSING COL-2006 INVALID_DATA_TYPE COL-2101 SCHEMA_NOT_FOUND COL-2102 SCHEMA_MISMATCH COL-3002 DB_INSERT_FAILED COL-4002 HUB_SEND_FAILED COL-5001 INTERNAL_ERROR 오류 발생 시 Error Channel에서 에러코드를 판별하여 DB에 저장할 수 있습니다. public void handle(ErrorMessage message) { Throwable error = message.getPayload(); // 오류 분석 // 오류 코드 생성 // DB 오류 이력 저장 } *** 12. 데이터 수집 시스템 적용 예시 Spring Integration은 특히 IoT 또는 도시데이터 수집 시스템에서 활용하기 좋습니다. 전체 흐름을 다음과 같이 구성할 수 있습니다. 장비 / 센서 / API ↓ Inbound Adapter ↓ receiveChannel ↓ 원본 데이터 저장 ↓ JSON Parser ↓ Schema Validator ↓ Transformer ↓ Router ┌────┼────┐ ↓ ↓ ↓ 환경 CCTV 교통 └────┼────┘ ↓ Oracle 저장 ↓ DataHub 변환 ↓ Outbound Adapter ↓ 데이터허브 오류가 발생하면 별도의 흐름으로 전달합니다. 각 처리 단계 ↓ ERROR ↓ errorChannel ↓ 오류 코드 판별 ↓ 오류 이력 저장 *** 13. JSON 원본 데이터를 CLOB으로 저장하는 구조 수집 시스템에서는 수신한 원본 데이터를 그대로 보존하는 것이 중요합니다. 예를 들어 다음 JSON을 수신했다고 가정합니다. { "deviceId": "DEV001", "temperature": 25.3, "humidity": 60 } Oracle에서는 원본 데이터를 "CLOB" 컬럼에 저장할 수 있습니다. CREATE TABLE TBL_COLLECT_DATA ( COLLECT_NO NUMBER , DEVICE_ID VARCHAR2(50) , COLLECT_STATUS VARCHAR2(20) , ERROR_CODE VARCHAR2(20) , ERROR_MSG VARCHAR2(1000) , RAW_DATA CLOB , CREATE_AT DATE ); Java에서는 CLOB을 직접 다루기보다 일반적으로 "String"으로 처리합니다. public class CollectData { private Long collectNo; private String deviceId; private String collectStatus; private String errorCode; private String errorMsg; private String rawData; } 수신 데이터 처리 순서는 다음과 같이 구성할 수 있습니다. JSON 수신 ↓ RAW_DATA CLOB 저장 ↓ JSON Parsing ↓ Schema 검증 ↓ 데이터 변환 ↓ 처리 이 구조의 장점은 JSON Parsing에 실패해도 수신 원본 데이터가 남아 있다는 것입니다. 예를 들어 잘못된 JSON이 들어왔다면 다음과 같은 상태를 남길 수 있습니다. COLLECT_STATUS = FAIL ERROR_CODE = COL-2003 ERROR_MSG = JSON_PARSE_ERROR RAW_DATA = 실제 수신한 원본 데이터 이를 통해 운영자가 장애 원인을 확인하거나 나중에 데이터를 재처리할 수 있습니다. *** 14. 일반 Spring Service 방식과의 차이 Spring Integration을 반드시 사용해야 하는 것은 아닙니다. 단순한 시스템이라면 기존 Spring Service 방식이 오히려 더 단순합니다. 일반 Spring public void collect(String data) { parse(data); validate(data); save(data); send(data); } 하지만 처리 종류가 많아지면 다음과 같은 코드가 생길 수 있습니다. if ("TCP".equals(protocol)) { if ("ENV".equals(serviceType)) { // 환경센서 처리 } else if ("CCTV".equals(serviceType)) { // CCTV 처리 } } else if ("HTTP".equals(protocol)) { // HTTP 처리 } 서비스 종류와 프로토콜이 증가할수록 관리가 어려워집니다. Spring Integration에서는 이를 각각의 Flow로 분리할 수 있습니다. TCP Inbound Flow HTTP Inbound Flow MQTT Inbound Flow Parsing Flow Validation Flow Environment Flow CCTV Flow Traffic Flow Storage Flow DataHub Flow Error Flow *** 15. Spring Integration의 장점 처리 흐름을 명확하게 표현할 수 있다 Receive ↓ Parse ↓ Validate ↓ Transform ↓ Save ↓ Send 데이터가 어떤 순서로 처리되는지 쉽게 확인할 수 있습니다. *** 외부 시스템 연동이 편리하다 Adapter를 이용하여 다양한 시스템과 연결할 수 있습니다. HTTP TCP UDP MQTT Kafka JMS FTP SFTP File 프로토콜별 연결 코드와 비즈니스 로직을 분리할 수 있습니다. *** 데이터 변환 및 분기가 쉽다 Transformer와 Router를 이용하면 시스템별 데이터 규격을 표준화하기 쉽습니다. 센서 A ─┐ 센서 B ─┼→ Transformer → 내부 표준 데이터 센서 C ─┘ *** 오류 처리를 통합할 수 있다 각 Service마다 반복적으로 "try-catch"를 작성하기보다 Error Channel을 이용하여 공통 오류 흐름을 만들 수 있습니다. 모든 처리 단계 ↓ Exception ↓ errorChannel ↓ 공통 오류 처리 *** 시스템 확장이 편리하다 기존에는 환경센서만 처리하다가 교통센서가 추가되더라도 별도의 Flow를 추가할 수 있습니다. 기존 └── Environment Flow 추가 ├── Environment Flow ├── Traffic Flow └── CCTV Flow *** 16. Spring Integration의 단점 Spring Integration이 모든 프로젝트에 필요한 것은 아닙니다. 학습해야 할 개념이 많다 일반적인 Spring MVC 개발과 달리 다음 개념을 이해해야 합니다. Message Channel Endpoint Adapter Gateway Router Transformer Filter Flow 처음 사용할 경우 구조가 오히려 복잡하게 느껴질 수 있습니다. *** 단순 CRUD에는 과하다 예를 들어 다음과 같은 기능에는 Spring Integration을 사용할 이유가 거의 없습니다. 회원 등록 게시판 조회 장비 정보 수정 공지사항 관리 이러한 기능은 일반적인 구조가 더 적합합니다. Controller ↓ Service ↓ Mapper ↓ Database Spring Integration은 시스템 연계와 데이터 흐름이 복잡한 영역에서 사용하는 것이 적합합니다.

August 12, 2026
KVM·libvirt 가상 머신에 Oracle 19c 설치하고 포트 포워딩 설정하기카테고리

KVM·libvirt 가상 머신에 Oracle 19c 설치하고 포트 포워딩 설정하기

KVM·libvirt 가상 머신에 Oracle 19c 설치하고 포트 포워딩 설정하기 <br />리눅스 서버에서 KVM과 libvirt를 이용하면 별도의 물리 서버 없이 가상 머신을 생성하고 운영할 수 있습니다. *** 1. 구성 환경 예제에서는 다음과 같은 환경을 사용합니다. 전체 통신 구조는 다음과 같습니다. 외부 사용자 │ ├── HOST_IP:22222 │ └── 192.168.122.2:22 │ └── HOST_IP:11521 └── 192.168.122.2:1521 «Oracle Database 19c 운영 환경은 Oracle Linux 또는 인증된 RHEL 계열 운영체제를 사용하는 것이 안전합니다. Ubuntu에 Oracle 19c를 설치할 경우 호환성 우회 설정과 추가 패키지가 필요할 수 있으므로 실습 환경에서 사용하는 것을 권장합니다.» *** 2. KVM 및 libvirt 설치 2.1 CPU 가상화 지원 확인 먼저 서버 CPU가 하드웨어 가상화를 지원하는지 확인합니다. egrep -c '(vmx|svm)' /proc/cpuinfo 출력값이 "1" 이상이면 CPU에서 가상화 기능을 지원하는 것입니다. * "vmx": Intel VT-x * "svm": AMD-V BIOS 또는 UEFI에서 가상화 기능이 비활성화되어 있다면 다음 항목을 활성화해야 합니다. Intel Virtualization Technology Intel VT-x AMD-V SVM Mode *** 2.2 KVM 관련 패키지 설치 Ubuntu 호스트에서 다음 패키지를 설치합니다. apt update apt install -y \ qemu-kvm \ libvirt-daemon-system \ libvirt-clients \ bridge-utils \ virtinst \ cpu-checker 각 패키지의 역할은 다음과 같습니다. *** 2.3 KVM 동작 확인 kvm-ok 정상적으로 사용할 수 있다면 다음과 비슷한 메시지가 출력됩니다. INFO: /dev/kvm exists KVM acceleration can be used KVM 디바이스도 확인합니다. ls -l /dev/kvm *** 2.4 libvirt 서비스 시작 systemctl enable --now libvirtd 서비스 상태를 확인합니다. systemctl status libvirtd 등록된 가상 머신 목록을 확인합니다. virsh list --all 아직 가상 머신을 생성하지 않았다면 빈 목록이 출력되는 것이 정상입니다. *** 3. libvirt 기본 네트워크 확인 libvirt는 일반적으로 "default"라는 NAT 네트워크를 사용합니다. virsh net-list --all 다음과 같이 "default" 네트워크가 표시되어야 합니다. Name State Autostart Persistent ------------------------------------------------ default active yes yes 네트워크가 존재하지만 비활성화되어 있다면 시작합니다. virsh net-start default virsh net-autostart default *** 3.1 default 네트워크가 없는 경우 다음과 같은 오류가 발생할 수 있습니다. error: Network not found: no network with matching name 'default' 이 경우 기본 NAT 네트워크 정의 파일을 생성합니다. cat <<'EOF' > /tmp/default-network.xml <network> <name>default</name> <forward mode='nat'/> <bridge name='virbr0' stp='on' delay='0'/> <ip address='192.168.122.1' netmask='255.255.255.0'> <dhcp> <range start='192.168.122.2' end='192.168.122.254'/> </dhcp> </ip> </network> EOF 네트워크를 정의하고 시작합니다. virsh net-define /tmp/default-network.xml virsh net-start default virsh net-autostart default 확인합니다. virsh net-list --all ip addr show virbr0 정상적으로 생성되면 "virbr0" 인터페이스에 다음 주소가 설정됩니다. 192.168.122.1/24 *** 4. libvirt Storage Pool 생성 스토리지 풀은 가상 머신 디스크 이미지를 저장하고 관리하는 공간입니다. 가상 머신 이미지 디렉터리를 생성합니다. mkdir -p /var/lib/libvirt/images "default" 스토리지 풀을 정의합니다. virsh pool-define-as \ default \ dir \ --target /var/lib/libvirt/images 스토리지 풀을 시작합니다. virsh pool-start default 서버 재부팅 후에도 자동으로 시작되도록 설정합니다. virsh pool-autostart default 스토리지 풀 상태를 확인합니다. virsh pool-list --all 정상적인 출력 예시는 다음과 같습니다. Name State Autostart -------------------------------- default active yes 스토리지 풀 상세 정보도 확인할 수 있습니다. virsh pool-info default *** 4.1 이미 default 풀이 존재하는 경우 다음 오류가 발생한다면 기존 스토리지 풀이 이미 존재하는 것입니다. error: storage pool 'default' already exists 기존 설정을 확인합니다. virsh pool-dumpxml default virsh pool-info default 잘못된 풀이 등록되어 있다면 먼저 제거한 후 다시 생성할 수 있습니다. virsh pool-destroy default virsh pool-undefine default «스토리지 풀을 삭제하기 전에 기존 가상 머신 디스크가 사용 중인지 반드시 확인해야 합니다.» *** 5. 가상 머신 생성 ISO 파일을 스토리지 경로에 복사합니다. cp ubuntu-24.04.iso /var/lib/libvirt/images/ 다음 명령으로 가상 머신을 생성합니다. virt-install \ --name ubuntu24 \ --memory 4096 \ --vcpus 2 \ --disk path=/var/lib/libvirt/images/ubuntu24.qcow2,size=40,format=qcow2 \ --cdrom /var/lib/libvirt/images/ubuntu-24.04.iso \ --os-variant ubuntu24.04 \ --network network=default,model=virtio \ --graphics vnc 각 옵션의 의미는 다음과 같습니다. *** 5.1 가상 머신 상태 확인 virsh list --all 가상 머신을 시작합니다. virsh start ubuntu24 가상 머신을 종료합니다. virsh shutdown ubuntu24 강제로 종료하려면 다음 명령을 사용합니다. virsh destroy ubuntu24 «"virsh destroy"는 전원 버튼을 강제로 끄는 것과 같으므로 정상 종료가 되지 않을 때만 사용해야 합니다.» 자동 시작을 설정합니다. virsh autostart ubuntu24 *** 5.2 VNC 접속 정보 확인 virsh vncdisplay ubuntu24 예를 들어 다음과 같이 출력될 수 있습니다. :0 이 경우 기본 VNC 포트는 다음과 같습니다. 5900 + 화면 번호 따라서 ":0"은 "5900", ":1"은 "5901"입니다. 보안을 위해 VNC 포트를 외부에 직접 공개하기보다는 SSH 터널을 사용하는 것이 좋습니다. ssh -L 5900:127.0.0.1:5900 사용자명@호스트IP *** 6. 가상 머신 IP 확인 및 고정 가상 머신 IP를 확인합니다. virsh domifaddr ubuntu24 또는 가상 머신 내부에서 확인합니다. ip addr 예제에서는 가상 머신 IP를 다음과 같이 사용합니다. 192.168.122.2 포트 포워딩 규칙은 특정 IP를 기준으로 설정되므로 가상 머신 IP가 변경되지 않도록 고정해야 합니다. 가상 머신의 MAC 주소는 다음 명령으로 확인할 수 있습니다. virsh domiflist ubuntu24 출력 예시는 다음과 같습니다. Interface Type Source Model MAC ------------------------------------------------------ vnet0 network default virtio 52:54:00:12:34:56 DHCP 예약을 사용하려면 다음과 같이 설정합니다. virsh net-update default add ip-dhcp-host \ "<host mac='52:54:00:12:34:56' name='ubuntu24' ip='192.168.122.2'/>" \ --live \ --config 설정 내용을 확인합니다. virsh net-dumpxml default 설정 후 가상 머신의 DHCP 주소를 갱신하거나 재부팅합니다. virsh reboot ubuntu24 *** 7. Oracle Database 19c 설치 준비 Oracle 설치 작업은 가상 머신 내부에서 진행합니다. Oracle Linux를 사용한다면 사전 설정 패키지를 사용하는 것이 가장 간단합니다. dnf install -y oracle-database-preinstall-19c 해당 패키지를 사용할 수 없는 RHEL 계열 운영체제에서는 Oracle 설치에 필요한 패키지와 커널 파라미터를 별도로 설정해야 합니다. 대표적인 필수 패키지는 다음과 같습니다. dnf install -y \ bc \ binutils \ elfutils-libelf \ elfutils-libelf-devel \ fontconfig-devel \ glibc \ glibc-devel \ ksh \ libaio \ libaio-devel \ libnsl \ libX11 \ libXau \ libXi \ libXtst \ libXrender \ libXrender-devel \ libgcc \ libstdc++ \ libstdc++-devel \ libxcb \ make \ smartmontools \ sysstat \ unzip «운영체제 버전에 따라 패키지 이름이 다르거나 추가 저장소가 필요할 수 있습니다.» *** 7.1 Oracle 그룹 및 계정 생성 Oracle 설치에 사용할 그룹을 생성합니다. groupadd oinstall groupadd dba Oracle 계정이 없다면 생성합니다. useradd -m -g oinstall -G dba oracle passwd oracle 이미 Oracle 계정이 존재한다면 기본 그룹과 보조 그룹을 변경합니다. usermod -g oinstall -aG dba oracle 계정 정보를 확인합니다. id oracle 정상적인 출력 예시는 다음과 같습니다. uid=1001(oracle) gid=1001(oinstall) groups=1001(oinstall),1002(dba) *** 7.2 Oracle 설치 디렉터리 생성 mkdir -p /u01/app/oracle/product/19.0.0/dbhome_1 mkdir -p /u01/app/oraInventory mkdir -p /u01/app/oracle/oradata mkdir -p /u01/app/oracle/fast_recovery_area 소유권과 권한을 변경합니다. chown -R oracle:oinstall /u01/app chmod -R 775 /u01/app 디렉터리 권한을 확인합니다. ls -ld /u01/app ls -ld /u01/app/oracle ls -ld /u01/app/oraInventory *** 8. Oracle 환경 변수 설정 Oracle 계정으로 전환합니다. su - oracle Oracle 계정의 ".bash_profile"을 수정합니다. vi ~/.bash_profile 다음 내용을 추가합니다. export ORACLE_BASE=/u01/app/oracle export ORACLE_HOME=/u01/app/oracle/product/19.0.0/dbhome_1 export ORACLE_SID=ORCL export PATH=$ORACLE_HOME/bin:$PATH export LD_LIBRARY_PATH=$ORACLE_HOME/lib:/lib64:/usr/lib64 export NLS_LANG=KOREAN_KOREA.AL32UTF8 운영체제 호환성 검사 우회가 필요한 실습 환경에서는 다음 값을 추가할 수 있습니다. export CV_ASSUME_DISTID=OEL8.4 «"CV_ASSUME_DISTID"는 설치 프로그램의 운영체제 검사를 우회하는 설정일 뿐, 해당 운영체제를 Oracle이 공식적으로 지원한다는 의미는 아닙니다.» 변경 내용을 적용합니다. source ~/.bash_profile 환경 변수를 확인합니다. echo $ORACLE_BASE echo $ORACLE_HOME echo $ORACLE_SID echo $PATH *** 9. Oracle 19c 설치 파일 압축 해제 Oracle 설치 파일을 Oracle 계정의 홈 디렉터리에 업로드합니다. /home/oracle/LINUX.X64_193000_db_home.zip Oracle Home 디렉터리로 이동합니다. cd /u01/app/oracle/product/19.0.0/dbhome_1 압축을 해제합니다. unzip /home/oracle/LINUX.X64_193000_db_home.zip 압축 해제 후 설치 파일을 확인합니다. ls -l 다음 파일이 존재해야 합니다. runInstaller *** 10. Oracle Database 19c 소프트웨어 설치 Oracle 계정으로 다음 명령을 실행합니다. cd "$ORACLE_HOME" 자동 설치를 실행합니다. ./runInstaller \ -silent \ -waitforcompletion \ -responseFile "$ORACLE_HOME/install/response/db_install.rsp" \ oracle.install.option=INSTALL_DB_SWONLY \ UNIX_GROUP_NAME=oinstall \ INVENTORY_LOCATION=/u01/app/oraInventory \ ORACLE_HOME="$ORACLE_HOME" \ ORACLE_BASE="$ORACLE_BASE" \ oracle.install.db.InstallEdition=EE \ oracle.install.db.OSDBA_GROUP=dba \ oracle.install.db.OSOPER_GROUP=dba \ oracle.install.db.OSBACKUPDBA_GROUP=dba \ oracle.install.db.OSDGDBA_GROUP=dba \ oracle.install.db.OSKMDBA_GROUP=dba \ oracle.install.db.OSRACDBA_GROUP=dba \ SECURITY_UPDATES_VIA_MYORACLESUPPORT=false \ DECLINE_SECURITY_UPDATES=true 주요 설정은 다음과 같습니다. 설치 로그는 일반적으로 다음 경로에서 확인할 수 있습니다. ls -ltr /u01/app/oraInventory/logs *** 10.1 루트 스크립트 실행 설치 완료 메시지에 표시되는 루트 스크립트를 실행해야 합니다. Oracle 계정에서 빠져나옵니다. exit root 계정으로 다음 스크립트를 실행합니다. /u01/app/oraInventory/orainstRoot.sh /u01/app/oracle/product/19.0.0/dbhome_1/root.sh 두 스크립트가 모두 정상적으로 완료되어야 Oracle 소프트웨어 설치가 끝납니다. *** 11. Oracle Listener 생성 Oracle 계정으로 전환합니다. su - oracle NETCA를 이용해 Listener를 생성합니다. netca -silent \ -responseFile "$ORACLE_HOME/assistants/netca/netca.rsp" Listener 상태를 확인합니다. lsnrctl status Listener가 실행 중이 아니라면 시작합니다. lsnrctl start Listener를 중지하려면 다음 명령을 사용합니다. lsnrctl stop Listener가 사용하는 포트를 확인합니다. ss -lntp | grep 1521 일반적으로 다음 주소에서 수신합니다. 0.0.0.0:1521 또는 서버의 특정 호스트명과 IP에서 수신할 수 있습니다. *** 12. Oracle 데이터베이스 생성 DBCA를 이용해 컨테이너 데이터베이스와 PDB를 생성합니다. dbca -silent -createDatabase \ -templateName General_Purpose.dbc \ -gdbname ORCL \ -sid ORCL \ -createAsContainerDatabase true \ -numberOfPDBs 1 \ -pdbName ORCLPDB \ -sysPassword 'Oracle비밀번호변경1!' \ -systemPassword 'Oracle비밀번호변경1!' \ -pdbAdminPassword 'Oracle비밀번호변경1!' \ -databaseType MULTIPURPOSE \ -memoryMgmtType auto_sga \ -totalMemory 2048 \ -storageType FS \ -datafileDestination /u01/app/oracle/oradata \ -recoveryAreaDestination /u01/app/oracle/fast_recovery_area \ -characterSet AL32UTF8 \ -nationalCharacterSet AL16UTF16 \ -emConfiguration NONE «예제의 비밀번호는 반드시 실제 운영 환경에서 다른 강력한 비밀번호로 변경해야 합니다.» 주요 옵션은 다음과 같습니다. 데이터베이스 생성은 서버 성능에 따라 시간이 걸릴 수 있습니다. *** 12.1 PDB 상태 확인 SYSDBA 권한으로 접속합니다. sqlplus / as sysdba PDB 상태를 확인합니다. SHOW PDBS; PDB가 닫혀 있다면 실행합니다. ALTER PLUGGABLE DATABASE ORCLPDB OPEN; 데이터베이스 재시작 후에도 PDB가 자동으로 열리도록 상태를 저장합니다. ALTER PLUGGABLE DATABASE ORCLPDB SAVE STATE; PDB 상태를 조회합니다. SELECT NAME, OPEN_MODE FROM V$PDBS; 정상적인 상태는 다음과 같습니다. ORCLPDB READ WRITE SQL*Plus를 종료합니다. EXIT; *** 13. Oracle 서비스 자동 시작 설정 13.1 oratab 설정 root 계정으로 "/etc/oratab" 파일을 수정합니다. vi /etc/oratab 다음과 같이 마지막 값을 "Y"로 변경합니다. ORCL:/u01/app/oracle/product/19.0.0/dbhome_1:Y 마지막 값의 의미는 다음과 같습니다. *** 13.2 systemd 서비스 파일 생성 vi /etc/systemd/system/oracle-db.service 다음 내용을 입력합니다. [Unit] Description=Oracle Database 19c Wants=network-online.target After=network-online.target [Service] Type=oneshot User=oracle Group=oinstall Environment=ORACLE_BASE=/u01/app/oracle Environment=ORACLE_HOME=/u01/app/oracle/product/19.0.0/dbhome_1 Environment=ORACLE_SID=ORCL Environment=PATH=/u01/app/oracle/product/19.0.0/dbhome_1/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin ExecStart=/bin/bash -lc '$ORACLE_HOME/bin/lsnrctl start || true; $ORACLE_HOME/bin/dbstart $ORACLE_HOME' ExecStop=/bin/bash -lc '$ORACLE_HOME/bin/dbshut $ORACLE_HOME; $ORACLE_HOME/bin/lsnrctl stop || true' RemainAfterExit=yes TimeoutStartSec=600 TimeoutStopSec=600 [Install] WantedBy=multi-user.target systemd 설정을 다시 읽습니다. systemctl daemon-reload Oracle 서비스를 활성화합니다. systemctl enable oracle-db 서비스를 시작합니다. systemctl start oracle-db 상태를 확인합니다. systemctl status oracle-db *** 14. Oracle 데이터베이스 실행 상태 확인 Oracle PMON 프로세스를 확인합니다. ps -ef | grep pmon | grep -v grep 정상적인 출력 예시는 다음과 같습니다. oracle 12345 1 0 10:00 ? 00:00:00 ora_pmon_ORCL Listener 상태도 확인합니다. su - oracle -c "lsnrctl status" Oracle 포트가 열려 있는지 확인합니다. ss -lntp | grep 1521 Oracle 인스턴스 상태를 확인합니다. su - oracle sqlplus / as sysdba SELECT INSTANCE_NAME, STATUS FROM V$INSTANCE; 정상적인 상태는 다음과 같습니다. INSTANCE_NAME STATUS --------------- ------------ ORCL OPEN *** 15. 가상 머신 내부 방화벽 설정 가상 머신이 RHEL 계열이고 "firewalld"를 사용한다면 SSH와 Oracle 포트를 허용합니다. firewall-cmd --permanent --add-service=ssh firewall-cmd --permanent --add-port=1521/tcp firewall-cmd --reload 설정을 확인합니다. firewall-cmd --list-all Ubuntu 가상 머신에서 UFW를 사용한다면 다음과 같이 설정합니다. ufw allow 22/tcp ufw allow 1521/tcp ufw reload *** 16. 호스트의 IPv4 포워딩 활성화 호스트에서 가상 머신으로 패킷을 전달하려면 IPv4 포워딩이 활성화되어 있어야 합니다. 현재 설정을 확인합니다. sysctl net.ipv4.ip_forward 다음과 같이 출력되어야 합니다. net.ipv4.ip_forward = 1 값이 "0"이라면 즉시 활성화합니다. sysctl -w net.ipv4.ip_forward=1 재부팅 후에도 유지되도록 설정 파일을 생성합니다. cat <<'EOF' > /etc/sysctl.d/99-vm-forward.conf net.ipv4.ip_forward = 1 EOF 설정을 적용합니다. sysctl --system *** 17. iptables 포트 포워딩 설정 외부에서 호스트의 특정 포트로 접속했을 때 가상 머신으로 전달하도록 구성합니다. 포트 포워딩 구성은 다음과 같습니다. «iptables의 테이블 이름은 대소문자를 구분하므로 "NAT"가 아니라 반드시 "nat"로 작성해야 합니다.» *** 17.1 사용자 정의 체인 생성 이미 체인이 존재해도 오류로 중단되지 않도록 다음과 같이 실행합니다. iptables -t nat -N VM_PREROUTING 2>/dev/null || true iptables -t nat -N VM_POSTROUTING 2>/dev/null || true iptables -N VM_FORWARD 2>/dev/null || true 기존 체인 규칙을 초기화합니다. iptables -t nat -F VM_PREROUTING iptables -t nat -F VM_POSTROUTING iptables -F VM_FORWARD *** 17.2 DNAT 규칙 추가 호스트의 "22222" 포트를 가상 머신의 SSH 포트로 전달합니다. iptables -t nat -A VM_PREROUTING \ -p tcp \ --dport 22222 \ -j DNAT \ --to-destination 192.168.122.2:22 호스트의 "11521" 포트를 가상 머신의 Oracle 포트로 전달합니다. iptables -t nat -A VM_PREROUTING \ -p tcp \ --dport 11521 \ -j DNAT \ --to-destination 192.168.122.2:1521 외부 인터페이스가 "eno1"로 확정되어 있다면 다음과 같이 인터페이스를 제한할 수도 있습니다. iptables -t nat -A VM_PREROUTING \ -i eno1 \ -p tcp \ --dport 22222 \ -j DNAT \ --to-destination 192.168.122.2:22 iptables -t nat -A VM_PREROUTING \ -i eno1 \ -p tcp \ --dport 11521 \ -j DNAT \ --to-destination 192.168.122.2:1521 «인터페이스를 지정한 규칙과 지정하지 않은 규칙을 동시에 등록하지 말고 환경에 맞는 한 가지 방식만 사용합니다.» 외부 인터페이스 이름은 다음 명령으로 확인할 수 있습니다. ip route | grep default *** 17.3 POSTROUTING 설정 가상 머신 네트워크에서 외부로 나가는 패킷에 NAT를 적용합니다. iptables -t nat -A VM_POSTROUTING \ -s 192.168.122.0/24 \ -j MASQUERADE libvirt 기본 NAT 네트워크가 이미 MASQUERADE 규칙을 생성한 경우 이 규칙은 중복될 수 있습니다. 기존 규칙을 먼저 확인합니다. iptables -t nat -L POSTROUTING -n -v *** 17.4 FORWARD 허용 규칙 추가 이미 연결된 세션의 응답 패킷을 허용합니다. iptables -A VM_FORWARD \ -m conntrack \ --ctstate ESTABLISHED,RELATED \ -j ACCEPT 가상 머신 SSH 접속을 허용합니다. iptables -A VM_FORWARD \ -p tcp \ -d 192.168.122.2 \ --dport 22 \ -m conntrack \ --ctstate NEW \ -j ACCEPT Oracle Listener 접속을 허용합니다. iptables -A VM_FORWARD \ -p tcp \ -d 192.168.122.2 \ --dport 1521 \ -m conntrack \ --ctstate NEW \ -j ACCEPT *** 17.5 사용자 정의 체인 연결 PREROUTING 체인에 사용자 정의 체인을 연결합니다. iptables -t nat -C PREROUTING -j VM_PREROUTING 2>/dev/null || \ iptables -t nat -A PREROUTING -j VM_PREROUTING POSTROUTING 체인에 사용자 정의 체인을 연결합니다. iptables -t nat -C POSTROUTING -j VM_POSTROUTING 2>/dev/null || \ iptables -t nat -A POSTROUTING -j VM_POSTROUTING FORWARD 체인의 첫 번째 위치에 사용자 정의 체인을 연결합니다. iptables -C FORWARD -j VM_FORWARD 2>/dev/null || \ iptables -I FORWARD 1 -j VM_FORWARD *** 17.6 전체 설정 명령 전체 명령을 한 번에 정리하면 다음과 같습니다. # IPv4 포워딩 활성화 sysctl -w net.ipv4.ip_forward=1 # 사용자 정의 체인 생성 iptables -t nat -N VM_PREROUTING 2>/dev/null || true iptables -t nat -N VM_POSTROUTING 2>/dev/null || true iptables -N VM_FORWARD 2>/dev/null || true # 기존 사용자 정의 체인 초기화 iptables -t nat -F VM_PREROUTING iptables -t nat -F VM_POSTROUTING iptables -F VM_FORWARD # 포트 포워딩 iptables -t nat -A VM_PREROUTING \ -p tcp --dport 22222 \ -j DNAT --to-destination 192.168.122.2:22 iptables -t nat -A VM_PREROUTING \ -p tcp --dport 11521 \ -j DNAT --to-destination 192.168.122.2:1521 # 가상 머신 외부 통신 NAT iptables -t nat -A VM_POSTROUTING \ -s 192.168.122.0/24 \ -j MASQUERADE # 응답 패킷 허용 iptables -A VM_FORWARD \ -m conntrack \ --ctstate ESTABLISHED,RELATED \ -j ACCEPT # SSH 허용 iptables -A VM_FORWARD \ -p tcp \ -d 192.168.122.2 \ --dport 22 \ -m conntrack \ --ctstate NEW \ -j ACCEPT # Oracle 허용 iptables -A VM_FORWARD \ -p tcp \ -d 192.168.122.2 \ --dport 1521 \ -m conntrack \ --ctstate NEW \ -j ACCEPT # 사용자 정의 체인 연결 iptables -t nat -C PREROUTING -j VM_PREROUTING 2>/dev/null || \ iptables -t nat -A PREROUTING -j VM_PREROUTING iptables -t nat -C POSTROUTING -j VM_POSTROUTING 2>/dev/null || \ iptables -t nat -A POSTROUTING -j VM_POSTROUTING iptables -C FORWARD -j VM_FORWARD 2>/dev/null || \ iptables -I FORWARD 1 -j VM_FORWARD *** 18. iptables 규칙 확인 NAT 규칙을 확인합니다. iptables -t nat -L -n -v --line-numbers PREROUTING 사용자 정의 체인을 확인합니다. iptables -t nat -L VM_PREROUTING -n -v --line-numbers POSTROUTING 사용자 정의 체인을 확인합니다. iptables -t nat -L VM_POSTROUTING -n -v --line-numbers FORWARD 규칙을 확인합니다. iptables -L VM_FORWARD -n -v --line-numbers 포트에 접속하면 "pkts", "bytes" 값이 증가해야 합니다. *** 19. iptables 설정 영구 저장 Ubuntu에서 iptables 규칙은 서버를 재부팅하면 초기화될 수 있습니다. 영구 저장 패키지를 설치합니다. apt install -y iptables-persistent 현재 규칙을 저장합니다. netfilter-persistent save 또는 직접 저장할 수 있습니다. iptables-save > /etc/iptables/rules.v4 저장된 규칙을 다시 불러오려면 다음 명령을 사용합니다. iptables-restore < /etc/iptables/rules.v4 서비스 상태를 확인합니다. systemctl status netfilter-persistent *** 20. 접속 테스트 20.1 호스트에서 가상 머신 접속 확인 먼저 호스트에서 가상 머신으로 직접 접속되는지 확인합니다. SSH 포트 확인: nc -vz 192.168.122.2 22 Oracle 포트 확인: nc -vz 192.168.122.2 1521 직접 접속이 되지 않는다면 포트 포워딩보다 먼저 가상 머신의 서비스와 방화벽을 확인해야 합니다. *** 20.2 외부에서 SSH 접속 외부 PC에서 다음과 같이 접속합니다. ssh -p 22222 사용자명@호스트IP 예시는 다음과 같습니다. ssh -p 22222 oracle@192.168.0.10 *** 20.3 외부에서 Oracle 포트 확인 nc -vz 호스트IP 11521 Windows PowerShell에서는 다음 명령을 사용할 수 있습니다. Test-NetConnection 호스트IP -Port 11521 *** 20.4 SQL*Plus 접속 sqlplus system/'Oracle비밀번호변경1!'@//호스트IP:11521/ORCLPDB SID가 아니라 PDB 서비스명인 "ORCLPDB"를 사용한다는 점에 주의합니다. *** 20.5 JDBC 접속 문자열 Spring Boot 또는 Java 애플리케이션에서는 다음 형식을 사용합니다. spring.datasource.url=jdbc:oracle:thin:@//호스트IP:11521/ORCLPDB spring.datasource.username=SYSTEM spring.datasource.password=Oracle비밀번호변경1! spring.datasource.driver-class-name=oracle.jdbc.OracleDriver JDBC URL 형식은 다음과 같습니다. jdbc:oracle:thin:@//호스트IP:11521/ORCLPDB *** 21. 주요 문제 해결 방법 21.1 Storage Pool을 찾을 수 없는 경우 오류 메시지: error: Storage pool not found: no storage pool with matching name 'default' 스토리지 풀을 생성합니다. mkdir -p /var/lib/libvirt/images virsh pool-define-as \ default \ dir \ --target /var/lib/libvirt/images virsh pool-start default virsh pool-autostart default *** 21.2 default 네트워크를 찾을 수 없는 경우 virsh net-list --all 네트워크가 존재하면 시작합니다. virsh net-start default virsh net-autostart default 네트워크 자체가 없다면 앞에서 설명한 XML 파일을 이용해 새로 정의합니다. *** 21.3 가상 머신 IP가 변경되는 경우 iptables는 "192.168.122.2"로 전달하도록 설정되어 있습니다. VM IP가 변경되면 포트 포워딩이 동작하지 않습니다. 다음을 확인합니다. virsh domifaddr ubuntu24 virsh net-dhcp-leases default 해결 방법은 다음 중 하나입니다. 1. libvirt DHCP 예약 설정 2. 가상 머신 내부에서 고정 IP 설정 3. 변경된 IP에 맞게 iptables DNAT 규칙 수정 *** 21.4 Oracle 1521 포트가 열리지 않는 경우 Listener 상태를 확인합니다. su - oracle lsnrctl status Listener를 시작합니다. lsnrctl start 포트를 확인합니다. ss -lntp | grep 1521 가상 머신 방화벽도 확인합니다. firewall-cmd --list-all *** 21.5 ORA-12514 오류가 발생하는 경우 대표적인 오류 메시지는 다음과 같습니다. ORA-12514: TNS:listener does not currently know of service requested Listener에 등록된 서비스명을 확인합니다. lsnrctl services PDB 상태를 확인합니다. sqlplus / as sysdba SHOW PDBS; PDB를 실행하고 상태를 저장합니다. ALTER PLUGGABLE DATABASE ORCLPDB OPEN; ALTER PLUGGABLE DATABASE ORCLPDB SAVE STATE; 접속 주소에는 SID인 "ORCL"이 아니라 서비스명인 "ORCLPDB"를 사용합니다. //호스트IP:11521/ORCLPDB *** 21.6 외부에서 포트 접속이 되지 않는 경우 IPv4 포워딩을 확인합니다. sysctl net.ipv4.ip_forward iptables 카운터를 확인합니다. iptables -t nat -L VM_PREROUTING -n -v iptables -L VM_FORWARD -n -v 패킷이 호스트까지 들어오는지 확인합니다. tcpdump -ni any port 22222 Oracle 포트는 다음과 같이 확인합니다. tcpdump -ni any port 11521 호스트 자체 방화벽도 확인해야 합니다. ufw status 필요한 경우 호스트 공개 포트를 허용합니다. ufw allow 22222/tcp ufw allow 11521/tcp 공유기나 상위 방화벽이 존재한다면 해당 장비에서도 포트 허용 또는 포트 포워딩 설정이 필요합니다. *** 21.7 Oracle 서비스가 부팅 후 실행되지 않는 경우 서비스 상태를 확인합니다. systemctl status oracle-db 로그를 확인합니다. journalctl -u oracle-db -b "/etc/oratab" 설정을 확인합니다. cat /etc/oratab 다음과 같이 마지막 값이 "Y"여야 합니다. ORCL:/u01/app/oracle/product/19.0.0/dbhome_1:Y Oracle 계정으로 직접 실행해 오류를 확인할 수도 있습니다. su - oracle dbstart "$ORACLE_HOME" lsnrctl start *** 22. 포트 포워딩 규칙 제거 사용자 정의 체인의 연결 규칙을 제거합니다. iptables -t nat -D PREROUTING -j VM_PREROUTING iptables -t nat -D POSTROUTING -j VM_POSTROUTING iptables -D FORWARD -j VM_FORWARD 체인 내부 규칙을 초기화합니다. iptables -t nat -F VM_PREROUTING iptables -t nat -F VM_POSTROUTING iptables -F VM_FORWARD 사용자 정의 체인을 삭제합니다. iptables -t nat -X VM_PREROUTING iptables -t nat -X VM_POSTROUTING iptables -X VM_FORWARD 변경된 규칙을 영구 저장합니다. netfilter-persistent save *** 23. 보안 설정 권장 사항 운영 환경에서는 모든 외부 IP에 Oracle 포트를 공개하지 않는 것이 좋습니다. 특정 관리 IP만 SSH에 접근하도록 제한할 수 있습니다. iptables -t nat -A VM_PREROUTING \ -p tcp \ -s 203.0.113.10 \ --dport 22222 \ -j DNAT \ --to-destination 192.168.122.2:22 Oracle 접속도 허용된 애플리케이션 서버 IP로 제한합니다. iptables -t nat -A VM_PREROUTING \ -p tcp \ -s 203.0.113.20 \ --dport 11521 \ -j DNAT \ --to-destination 192.168.122.2:1521 추가로 다음 보안 설정을 권장합니다. * SSH 비밀번호 인증 대신 공개 키 인증 사용 * root 원격 로그인 비활성화 * Oracle 기본 계정 비밀번호 변경 * Oracle 1521 포트의 전체 인터넷 공개 금지 * 허용할 외부 IP를 방화벽으로 제한 * 불필요한 VNC 포트 외부 공개 금지 * Oracle 데이터 및 설정 파일 정기 백업 * iptables 규칙 변경 전 기존 설정 백업 iptables 규칙은 다음 명령으로 백업할 수 있습니다. iptables-save > /root/iptables-backup-$(date +%Y%m%d).rules 복원 명령은 다음과 같습니다. iptables-restore < /root/iptables-backup-20260806.rules *** 24. 최종 확인 명령어 호스트에서 확인 virsh list --all virsh net-list --all virsh pool-list --all virsh domifaddr ubuntu24 sysctl net.ipv4.ip_forward iptables -t nat -L VM_PREROUTING -n -v iptables -L VM_FORWARD -n -v 가상 머신에서 확인 ps -ef | grep pmon | grep -v grep ss -lntp | grep 1521 systemctl status oracle-db su - oracle -c "lsnrctl status" 외부 PC에서 확인 ssh -p 22222 사용자명@호스트IP nc -vz 호스트IP 11521 Oracle 접속 주소는 다음과 같습니다. 호스트: 호스트 서버 IP 포트: 11521 서비스명: ORCLPDB JDBC URL은 다음과 같습니다. jdbc:oracle:thin:@//호스트IP:11521/ORCLPDB

August 6, 2026
Linux iptables 완벽 가이드카테고리

Linux iptables 완벽 가이드

<br /> Linux iptables 완벽 가이드 리눅스 서버를 운영하다 보면 가장 많이 접하게 되는 보안 기능 중 하나가 iptables입니다. iptables는 서버로 들어오고 나가는 네트워크 패킷을 제어하여 허용하거나 차단하는 리눅스 방화벽(Firewall) 입니다. 이번 글에서는 iptables의 기본 개념부터 체인(Chain), NAT, 사용자 정의 체인, 실무 예제까지 한 번에 정리해보겠습니다. *** iptables란? iptables는 Linux Kernel의 Netfilter 프레임워크를 제어하는 사용자 공간(User Space) 프로그램입니다. 쉽게 말하면 * 특정 IP만 접속 허용 * 특정 포트 차단 * NAT(Network Address Translation) * 포트포워딩 * 패킷 필터링 등을 수행할 수 있습니다. 대표적인 활용 예시는 다음과 같습니다. * SSH(22)만 허용 * HTTP(80), HTTPS(443) 허용 * FTP(21) 차단 * 특정 국가 또는 IP 차단 * Docker, KVM NAT 구성 *** iptables 동작 구조 패킷은 서버에 도착하면 다음과 같은 순서로 처리됩니다. 인터넷 │ ▼ PREROUTING │ ┌─-────-───┴───-─────-──┐ ▼ ▼ INPUT FORWARD │ │ ▼ ▼ Local Host 다른 서버 전달 │ ▼ OUTPUT │ ▼ POSTROUTING │ ▼ 인터넷 *** Table과 Chain iptables는 여러 개의 Table과 Chain으로 구성됩니다. Table 실무에서는 대부분 filter와 nat를 사용합니다. *** Chain filter 테이블에는 기본적으로 3개의 Chain이 존재합니다. INPUT 인터넷 │ ▼ 내 서버 OUTPUT 내 서버 │ ▼ 인터넷 FORWARD 인터넷 │ ▼ 내 서버 │ ▼ 다른 서버 *** 기본 정책(Default Policy) 현재 정책 확인 iptables -L 또는 iptables -L -n 모든 INPUT 허용 iptables -P INPUT ACCEPT 모든 INPUT 차단 iptables -P INPUT DROP *** 규칙(Rule) iptables는 위에서 아래 순서대로 규칙을 검사합니다. 예를 들어 iptables -A INPUT -p tcp --dport 22 -j ACCEPT 의 의미는 * INPUT 체인 * TCP 프로토콜 * 22번 포트 * 허용 입니다. *** 자주 사용하는 옵션 *** 자주 사용하는 명령어 SSH 허용 iptables -A INPUT -p tcp --dport 22 -j ACCEPT *** HTTP 허용 iptables -A INPUT -p tcp --dport 80 -j ACCEPT *** HTTPS 허용 iptables -A INPUT -p tcp --dport 443 -j ACCEPT *** Ping 허용 iptables -A INPUT -p icmp -j ACCEPT *** 특정 IP 차단 iptables -A INPUT -s 192.168.0.100 -j DROP *** 특정 IP만 SSH 허용 iptables -A INPUT \ -p tcp \ -s 192.168.0.100 \ --dport 22 \ -j ACCEPT *** FTP 차단 iptables -A INPUT -p tcp --dport 21 -j DROP *** Telnet 차단 iptables -A INPUT -p tcp --dport 23 -j DROP *** Stateful Firewall 대부분의 서버에서는 이미 연결된 세션은 자동으로 허용합니다. iptables -A INPUT \ -m conntrack \ --ctstate ESTABLISHED,RELATED \ -j ACCEPT 이 규칙이 없으면 내 서버 → 인터넷 으로 요청을 보내도 응답 패킷이 차단될 수 있습니다. *** NAT KVM, Docker에서 가장 많이 사용하는 기능입니다. iptables -t nat \ -A POSTROUTING \ -o eth0 \ -j MASQUERADE 인터넷 공유기의 NAT 기능과 동일한 역할을 수행합니다. *** 포트포워딩 80포트를 8080으로 변경 iptables -t nat \ -A PREROUTING \ -p tcp \ --dport 80 \ -j REDIRECT \ --to-port 8080 *** 사용자 정의 체인(User Defined Chain) iptables는 직접 체인을 만들어 관리할 수 있습니다. 실무에서는 서비스별로 체인을 분리하여 관리하는 경우가 많습니다. 예) * SSH_CHAIN * WEB_CHAIN * DB_CHAIN * LOG_DROP *** 체인 생성 WEB 체인 생성 iptables -N WEB 확인 iptables -L Chain WEB (0 references) *** 체인에 규칙 추가 iptables -A WEB -p tcp --dport 80 -j ACCEPT iptables -A WEB -p tcp --dport 443 -j ACCEPT 조회 iptables -L WEB -n *** INPUT에 연결(Jump) 체인을 생성했다고 자동 실행되지는 않습니다. INPUT에서 호출해야 합니다. iptables -A INPUT -j WEB 동작 순서 인터넷 ↓ INPUT ↓ WEB ↓ 80 → 허용 443 → 허용 그 외 → INPUT으로 복귀 *** RETURN 조건이 맞지 않으면 원래 체인으로 복귀합니다. iptables -A WEB \ -p tcp \ --dport 80 \ -j ACCEPT iptables -A WEB -j RETURN 흐름 INPUT ↓ WEB ↓ 80? YES → ACCEPT NO ↓ RETURN ↓ INPUT 다음 규칙 *** 체인 삭제 체인 비우기 iptables -F WEB INPUT에서 제거 iptables -D INPUT -j WEB 체인 삭제 iptables -X WEB *** 규칙 조회 패킷 수까지 보기 iptables -L -n -v 규칙 번호 보기 iptables -L --line-numbers *** 규칙 삭제 번호로 삭제 iptables -D INPUT 3 규칙으로 삭제 iptables -D INPUT \ -p tcp \ --dport 23 \ -j DROP *** 전체 삭제 iptables -F NAT 삭제 iptables -t nat -F *** 설정 저장 Rocky / CentOS iptables-save > /etc/sysconfig/iptables 또는 service iptables save *** Ubuntu iptables-save > /etc/iptables/rules.v4 일반적으로 "iptables-persistent" 패키지를 함께 사용합니다. ***

August 6, 2026
Linux 서버에 Oracle Database 19c 설치하기카테고리

Linux 서버에 Oracle Database 19c 설치하기

*** Linux 서버에 Oracle Database 19c 설치하기 이번 글에서는 Linux 서버에 Oracle Database 19c를 설치하는 방법을 정리한다. 서버 환경에서는 GUI 화면을 사용할 수 없는 경우가 많으므로, Oracle 설치 프로그램인 "runInstaller"를 Silent 모드로 실행한다. 설치 과정은 다음 순서로 진행한다. 1. 운영체제 및 서버 사양 확인 2. Oracle 설치에 필요한 패키지 설치 3. Oracle 그룹 및 사용자 생성 4. Oracle 설치 디렉터리 생성 5. 환경변수 설정 6. Oracle Database 소프트웨어 설치 7. Listener 생성 8. 데이터베이스 생성 9. 외부 접속 및 방화벽 설정 10. 설치 상태 확인 *** 1. 설치 환경 이번 글에서 사용하는 설치 환경은 다음과 같다. Oracle Database 19c 공식 지원 목록에는 Oracle Linux, Red Hat Enterprise Linux, SUSE Linux Enterprise Server 등이 포함되어 있다. Rocky Linux는 공식 지원 목록에 직접 명시되어 있지 않으므로 운영 환경에서는 Oracle Linux 또는 RHEL을 사용하는 것이 안전하다. «Rocky Linux에서도 설치를 시도할 수 있지만 운영체제 검사, 패키지 호환성, "libnsl.so.1" 등의 문제가 발생할 수 있다.» *** 2. 서버 사양 확인 Oracle Database 19c는 최소 1GB 이상의 메모리가 필요하며 2GB 이상이 권장된다. 실습용 가상머신이라면 최소 4GB 정도를 할당하는 것이 좋다. "/tmp"에는 최소 1GB 이상의 여유 공간이 필요하다. 서버 사양을 확인한다. free -h df -h df -h /tmp df -h /dev/shm uname -m 정상적인 64비트 x86 서버라면 다음과 같이 출력된다. x86_64 메모리와 Swap 크기를 별도로 확인하려면 다음 명령어를 사용한다. grep MemTotal /proc/meminfo grep SwapTotal /proc/meminfo 운영체제 정보도 확인한다. cat /etc/os-release uname -r *** 3. 호스트 이름 설정 Oracle 설치 전에 서버의 호스트 이름과 "/etc/hosts" 설정을 확인한다. hostnamectl hostname 필요한 경우 호스트 이름을 설정한다. hostnamectl set-hostname oracle19c.localdomain 서버 IP를 확인한다. ip addr "/etc/hosts" 파일을 수정한다. vi /etc/hosts 다음과 같이 서버 IP와 호스트 이름을 등록한다. 127.0.0.1 localhost localhost.localdomain 192.168.0.100 oracle19c.localdomain oracle19c "192.168.0.100" 부분은 실제 Oracle 서버 IP로 변경한다. 설정 결과를 확인한다. ping -c 3 oracle19c *** 4. 필수 패키지 설치 Oracle Linux 8 Oracle Linux에서는 Oracle이 제공하는 사전 설치 패키지를 사용하는 것이 가장 간단하다. dnf install -y oracle-database-preinstall-19c dnf install -y unzip "oracle-database-preinstall-19c" 패키지는 Oracle 설치에 필요한 패키지와 커널 파라미터, 사용자 제한 설정 등을 자동으로 구성한다. 설치 상태를 확인한다. rpm -qa | grep oracle-database-preinstall RHEL 또는 Rocky Linux 계열 Oracle Linux 사전 설치 패키지를 사용할 수 없다면 필요한 패키지를 직접 설치해야 한다. dnf install -y \ bc \ binutils \ elfutils-libelf \ elfutils-libelf-devel \ fontconfig \ gcc \ gcc-c++ \ glibc \ glibc-devel \ ksh \ libaio \ libaio-devel \ libgcc \ libnsl \ libstdc++ \ libstdc++-devel \ libX11 \ libXau \ libXi \ libXrender \ libXtst \ libxcb \ make \ policycoreutils \ policycoreutils-python-utils \ smartmontools \ sysstat \ unzip RHEL 9 계열에서는 다음 패키지가 추가로 필요할 수 있다. dnf install -y \ compat-openssl11 \ libxcrypt-compat \ libasan \ liblsan \ libibverbs \ librdmacm \ libvirt-libs RHEL 9에서 Oracle Database 19c를 설치할 경우 19.19 이상의 릴리스 업데이트가 필요하며, Oracle 공식 문서에 필요한 패키지 목록이 정리되어 있다. libnsl.so.1 오류 확인 설치 중 다음 오류가 발생할 수 있다. error while loading shared libraries: libnsl.so.1 이 경우 "libnsl" 패키지를 설치한다. dnf install -y libnsl 라이브러리가 등록되었는지 확인한다. ldconfig -p | grep libnsl *** 5. Oracle 그룹 생성 Oracle 설치를 위한 그룹을 생성한다. groupadd oinstall groupadd dba 각 그룹의 역할은 다음과 같다. *** 6. Oracle 사용자 생성 Oracle 설치 전용 사용자를 생성한다. useradd -g oinstall -G dba oracle Oracle 사용자 비밀번호를 설정한다. passwd oracle 사용자 정보를 확인한다. id oracle 정상적인 출력 예시는 다음과 같다. uid=1001(oracle) gid=1001(oinstall) groups=1001(oinstall),1002(dba) 기존 Oracle 사용자의 그룹 변경 "oracle" 사용자가 이미 존재한다면 다음 명령어를 사용한다. usermod -g oinstall -aG dba oracle 옵션의 의미는 다음과 같다. 다음 명령어도 사용할 수 있다. usermod -g oinstall -G dba oracle 하지만 "-G"만 사용하면 기존에 등록되어 있던 다른 보조 그룹이 제거될 수 있다. 기존 그룹을 유지하려면 "-aG"를 사용하는 것이 안전하다. 그룹 설정 후에는 "oracle" 사용자가 다시 로그인해야 변경 내용이 적용된다. su - oracle id *** 7. Oracle 설치 디렉터리 생성 "root" 사용자로 Oracle 설치 디렉터리를 생성한다. mkdir -p /u01/app/oracle/product/19.0.0/dbhome_1 mkdir -p /u01/app/oraInventory mkdir -p /u01/app/oracle/oradata mkdir -p /u01/app/oracle/fast_recovery_area 각 디렉터리의 용도는 다음과 같다. 디렉터리 소유자를 변경한다. chown -R oracle:oinstall /u01/app chmod -R 775 /u01/app 설정 결과를 확인한다. ls -ld /u01/app ls -ld /u01/app/oracle ls -ld /u01/app/oraInventory *** 8. Oracle 설치 파일 압축 해제 Oracle 공식 사이트에서 다음 설치 파일을 내려받는다. LINUX.X64_193000_db_home.zip Oracle Database 19c는 설치 파일을 최종 "ORACLE_HOME" 디렉터리에 압축 해제한 후 해당 위치에서 "runInstaller"를 실행하는 이미지 기반 설치 방식이다. 설치 파일을 서버로 전송한 후 압축을 해제한다. su - oracle cd /u01/app/oracle/product/19.0.0/dbhome_1 unzip /home/oracle/LINUX.X64_193000_db_home.zip 설치 파일을 확인한다. ls -l runInstaller ls -l install/response/db_install.rsp 정상적으로 압축이 해제되었다면 다음 파일이 존재한다. /u01/app/oracle/product/19.0.0/dbhome_1/runInstaller /u01/app/oracle/product/19.0.0/dbhome_1/install/response/db_install.rsp *** 9. Oracle 환경변수 설정 "oracle" 사용자의 환경변수를 설정한다. su - oracle vi ~/.bash_profile 파일 마지막에 다음 내용을 추가한다. export TMP=/tmp export TMPDIR=$TMP export ORACLE_BASE=/u01/app/oracle export ORACLE_HOME=/u01/app/oracle/product/19.0.0/dbhome_1 export ORACLE_SID=ORCL export PATH=$ORACLE_HOME/bin:$PATH export LD_LIBRARY_PATH=$ORACLE_HOME/lib:/lib64:/usr/lib64 export CLASSPATH=$ORACLE_HOME/jlib:$ORACLE_HOME/rdbms/jlib 환경변수를 적용한다. source ~/.bash_profile 설정값을 확인한다. echo $ORACLE_BASE echo $ORACLE_HOME echo $ORACLE_SID echo $PATH 정상적인 결과는 다음과 같다. /u01/app/oracle /u01/app/oracle/product/19.0.0/dbhome_1 ORCL *** 10. Oracle Database 소프트웨어 설치 Oracle 사용자로 전환한다. su - oracle source ~/.bash_profile Oracle Home으로 이동한다. cd $ORACLE_HOME 다음 명령어를 실행해 Oracle Database 소프트웨어를 설치한다. ./runInstaller \ -silent \ -waitforcompletion \ -responseFile "$ORACLE_HOME/install/response/db_install.rsp" \ oracle.install.option=INSTALL_DB_SWONLY \ UNIX_GROUP_NAME=oinstall \ INVENTORY_LOCATION=/u01/app/oraInventory \ ORACLE_HOME="$ORACLE_HOME" \ ORACLE_BASE="$ORACLE_BASE" \ oracle.install.db.InstallEdition=EE \ oracle.install.db.OSDBA_GROUP=dba \ oracle.install.db.OSOPER_GROUP=dba \ oracle.install.db.OSBACKUPDBA_GROUP=dba \ oracle.install.db.OSDGDBA_GROUP=dba \ oracle.install.db.OSKMDBA_GROUP=dba \ oracle.install.db.OSRACDBA_GROUP=dba \ SECURITY_UPDATES_VIA_MYORACLESUPPORT=false \ DECLINE_SECURITY_UPDATES=true "-silent" 옵션을 사용하면 "DISPLAY" 환경변수 없이도 설치할 수 있다. 응답 파일은 반드시 상대 경로가 아닌 절대 경로로 지정해야 한다. 설치 옵션 설명 Standard Edition 2를 설치하려면 다음 값을 사용한다. oracle.install.db.InstallEdition=SE2 *** 11. INS-32013 오류 해결 다음과 같은 오류가 발생할 수 있다. [FATAL] [INS-32013] Oracle 기본 위치가 비어 있습니다. 이 오류는 "ORACLE_BASE"가 설정되지 않았거나 "runInstaller" 실행 시 Oracle 기본 위치가 전달되지 않아 발생한다. 환경변수를 확인한다. echo $ORACLE_BASE echo $ORACLE_HOME 값이 비어 있다면 설정한다. export ORACLE_BASE=/u01/app/oracle export ORACLE_HOME=/u01/app/oracle/product/19.0.0/dbhome_1 다음과 같이 설치 명령어에도 "ORACLE_BASE"와 "ORACLE_HOME"을 명시한다. ./runInstaller \ -silent \ -responseFile "$ORACLE_HOME/install/response/db_install.rsp" \ ORACLE_BASE="$ORACLE_BASE" \ ORACLE_HOME="$ORACLE_HOME" 단, 위 명령어에는 설치에 필요한 나머지 옵션도 함께 지정해야 하므로 실제 설치 시에는 앞에서 작성한 전체 명령어를 사용한다. *** 12. root 스크립트 실행 Oracle 소프트웨어 설치가 완료되면 다음과 같은 안내가 출력된다. As a root user, execute the following script(s): 1. /u01/app/oraInventory/orainstRoot.sh 2. /u01/app/oracle/product/19.0.0/dbhome_1/root.sh "root" 사용자로 전환한다. su - 안내된 스크립트를 실행한다. /u01/app/oraInventory/orainstRoot.sh /u01/app/oracle/product/19.0.0/dbhome_1/root.sh Oracle Universal Installer 설치 후에는 "orainstRoot.sh"와 "root.sh"를 실행해야 한다. *** 13. Oracle Listener 생성 Oracle 데이터베이스에 외부 프로그램이 접속하려면 Listener가 필요하다. "oracle" 사용자로 전환한다. su - oracle source ~/.bash_profile NETCA를 Silent 모드로 실행한다. netca -silent \ -responseFile "$ORACLE_HOME/assistants/netca/netca.rsp" Listener 상태를 확인한다. lsnrctl status Listener가 실행되지 않았다면 다음 명령어로 시작한다. lsnrctl start 중지하려면 다음 명령어를 사용한다. lsnrctl stop 기본 Listener 포트는 "1521"이다. 포트가 열려 있는지 확인한다. ss -lntp | grep 1521 *** 14. Oracle 데이터베이스 생성 Oracle 소프트웨어를 설치한 후 DBCA를 사용해 데이터베이스를 생성한다. Oracle DBCA는 "-silent -createDatabase" 옵션을 사용해 화면 없이 데이터베이스를 생성할 수 있다. 다음 예제에서는 아래와 같이 데이터베이스를 생성한다. 다음 명령어를 실행한다. dbca -silent -createDatabase \ -templateName General_Purpose.dbc \ -gdbname ORCL \ -sid ORCL \ -createAsContainerDatabase true \ -numberOfPDBs 1 \ -pdbName ORCLPDB \ -sysPassword 'Oracle비밀번호변경1!' \ -systemPassword 'Oracle비밀번호변경1!' \ -pdbAdminPassword 'Oracle비밀번호변경1!' \ -databaseType MULTIPURPOSE \ -memoryMgmtType auto_sga \ -totalMemory 2048 \ -storageType FS \ -datafileDestination /u01/app/oracle/oradata \ -recoveryAreaDestination /u01/app/oracle/fast_recovery_area \ -characterSet AL32UTF8 \ -nationalCharacterSet AL16UTF16 \ -emConfiguration NONE «예제에 작성된 비밀번호는 반드시 실제 운영 환경에 맞는 안전한 비밀번호로 변경해야 한다.» 명령어에 비밀번호를 직접 작성하면 Shell History에 남을 수 있으므로 운영 환경에서는 응답 파일이나 별도의 보안 관리 방식을 사용하는 것이 좋다. *** 15. 데이터베이스 실행 상태 확인 Oracle 프로세스를 확인한다. ps -ef | grep pmon | grep -v grep 정상적으로 데이터베이스가 실행되고 있다면 다음과 같이 출력된다. oracle 12345 1 0 16:20 ? 00:00:00 ora_pmon_ORCL "sqlplus"로 접속한다. sqlplus / as sysdba 인스턴스 상태를 확인한다. SELECT instance_name, status FROM v$instance; 정상적인 결과는 다음과 같다. INSTANCE_NAME STATUS ---------------- ------------ ORCL OPEN 데이터베이스 이름과 상태를 확인한다. SELECT name, open_mode FROM v$database; *** 16. PDB 상태 확인 Oracle 19c에서는 CDB와 PDB 구조를 사용한다. PDB 목록을 확인한다. SHOW PDBS; 다음과 같이 "MOUNTED" 상태로 표시될 수 있다. CON_ID CON_NAME OPEN MODE ------ --------- ---------- 2 PDB$SEED READ ONLY 3 ORCLPDB MOUNTED PDB를 실행한다. ALTER PLUGGABLE DATABASE ORCLPDB OPEN; 서버가 다시 시작된 후에도 자동으로 열리도록 상태를 저장한다. ALTER PLUGGABLE DATABASE ORCLPDB SAVE STATE; 다시 확인한다. SHOW PDBS; 정상 상태는 다음과 같다. ORCLPDB READ WRITE SQL*Plus를 종료한다. EXIT; *** 17. Listener 서비스 등록 확인 Listener에 데이터베이스 서비스가 등록됐는지 확인한다. lsnrctl status 정상이라면 다음과 같은 서비스가 표시된다. Service "ORCL" has 1 instance(s). Service "ORCLPDB" has 1 instance(s). 서비스가 표시되지 않는다면 SQL*Plus에 접속한다. sqlplus / as sysdba 서비스를 Listener에 다시 등록한다. ALTER SYSTEM REGISTER; 다시 확인한다. lsnrctl status *** 18. 외부 접속을 위한 방화벽 설정 DBeaver, SQL Developer 등의 외부 프로그램에서 Oracle 서버에 접속하려면 TCP 1521 포트를 허용해야 한다. "root" 사용자로 실행한다. firewall-cmd --permanent --add-port=1521/tcp firewall-cmd --reload 허용된 포트를 확인한다. firewall-cmd --list-ports 다음과 같이 표시되면 정상이다. 1521/tcp 방화벽 서비스가 실행 중인지 확인한다. systemctl status firewalld *** . 20. 데이터베이스 시작 및 종료 데이터베이스 시작 sqlplus / as sysdba STARTUP; PDB도 실행한다. ALTER PLUGGABLE DATABASE ALL OPEN; 데이터베이스 종료 sqlplus / as sysdba SHUTDOWN IMMEDIATE; Listener 시작 lsnrctl start Listener 종료 lsnrctl stop Listener 재시작 lsnrctl stop lsnrctl start *** 21. 서버 시작 시 Oracle 자동 실행 설정 서버가 재부팅되었을 때 Oracle 데이터베이스가 자동으로 실행되도록 설정할 수 있다. "root" 사용자로 "/etc/oratab" 파일을 수정한다. vi /etc/oratab 다음 내용을 확인한다. ORCL:/u01/app/oracle/product/19.0.0/dbhome_1:N 마지막 값을 "N"에서 "Y"로 변경한다. ORCL:/u01/app/oracle/product/19.0.0/dbhome_1:Y Systemd 서비스 파일을 생성한다. vi /etc/systemd/system/oracle-db.service 다음 내용을 작성한다. [Unit] Description=Oracle Database 19c After=network.target [Service] Type=forking User=oracle Group=oinstall ExecStart=/u01/app/oracle/product/19.0.0/dbhome_1/bin/dbstart /u01/app/oracle/product/19.0.0/dbhome_1 ExecStop=/u01/app/oracle/product/19.0.0/dbhome_1/bin/dbshut /u01/app/oracle/product/19.0.0/dbhome_1 RemainAfterExit=yes TimeoutStartSec=300 TimeoutStopSec=300 [Install] WantedBy=multi-user.target Systemd 설정을 다시 읽는다. systemctl daemon-reload Oracle 서비스를 활성화하고 실행한다. systemctl enable oracle-db systemctl start oracle-db 상태를 확인한다. systemctl status oracle-db ***

August 5, 2026
Ubuntu에서 libvirt를 이용한 KVM 가상머신(VM) 생성 방법카테고리

Ubuntu에서 libvirt를 이용한 KVM 가상머신(VM) 생성 방법

Ubuntu에서 libvirt를 이용한 KVM 가상머신(VM) 생성 방법 개요 KVM(Kernel-based Virtual Machine)은 Linux 커널에 포함된 하이퍼바이저로, 리눅스 서버를 가상화 호스트(Hypervisor)로 사용할 수 있도록 지원합니다. 하지만 KVM만으로는 가상머신을 생성하거나 관리하기 어렵기 때문에 일반적으로 libvirt를 함께 사용합니다. libvirt는 KVM, QEMU 등의 가상화 기술을 통합 관리하는 프레임워크이며, "virsh", "virt-install", "virt-manager" 등의 도구는 모두 libvirt를 통해 가상머신을 제어합니다. *** KVM과 libvirt 구조 +---------------------+ | virt-manager | +----------+----------+ | +----------v----------+ | virsh | +----------+----------+ | +----------v----------+ | libvirt | +----------+----------+ | +----------v----------+ | QEMU/KVM | +----------+----------+ | +----------v----------+ | Virtual Machine | +---------------------+ 각 구성 요소의 역할은 다음과 같습니다. *** 1. CPU 가상화 지원 확인 먼저 CPU가 가상화를 지원하는지 확인합니다. egrep -c '(vmx|svm)' /proc/cpuinfo 또는 lscpu | grep Virtualization 출력 예시 Virtualization: VT-x * Intel → VT-x * AMD → AMD-V *** 2. KVM 설치 패키지 목록을 최신화합니다. sudo apt update 필요한 패키지를 설치합니다. sudo apt install -y \ qemu-kvm \ libvirt-daemon-system \ libvirt-clients \ bridge-utils \ virtinst \ cpu-checker *** 3. 설치 확인 kvm-ok 정상이라면 INFO: /dev/kvm exists KVM acceleration can be used *** 4. libvirt 서비스 실행 sudo systemctl enable --now libvirtd 상태 확인 systemctl status libvirtd *** 5. 사용자 권한 추가 현재 사용자를 libvirt 그룹에 추가합니다. sudo usermod -aG libvirt $USER sudo usermod -aG kvm $USER 로그아웃 후 다시 로그인하거나 newgrp libvirt 를 실행합니다. *** 6. libvirt 동작 확인 virsh list --all 출력 Id Name State *** 7. Storage Pool 생성 Ubuntu에서는 기본 Storage Pool이 없는 경우가 있습니다. 먼저 디렉터리를 생성합니다. sudo mkdir -p /var/lib/libvirt/images Storage Pool 정의 virsh pool-define-as \ default \ dir \ --target /var/lib/libvirt/images 시작 virsh pool-start default 자동 시작 virsh pool-autostart default 확인 virsh pool-list --all 예시 Name State Autostart -------------------------------- default active yes *** 8. 기본 Network 확인 virsh net-list --all 기본 네트워크가 비활성화되어 있다면 virsh net-start default 자동 시작 virsh net-autostart default *** 9. ISO 파일 준비 ISO 파일을 Storage Pool에 복사합니다. sudo cp ubuntu-24.04.iso /var/lib/libvirt/images/ *** 10. VM 생성 virt-install \ --name ubuntu24 \ --memory 4096 \ --vcpus 2 \ --disk path=/var/lib/libvirt/images/ubuntu24.qcow2,size=40 \ --cdrom /var/lib/libvirt/images/ubuntu-24.04.iso \ --os-variant ubuntu24.04 \ --network default \ --graphics vnc 옵션 설명 *** 11. VM 관리 전체 목록 virsh list --all 실행 virsh start ubuntu24 종료 virsh shutdown ubuntu24 강제 종료 virsh destroy ubuntu24 삭제 virsh undefine ubuntu24 자동 시작 virsh autostart ubuntu24 *** 12. VM 콘솔 접속 virsh console ubuntu24 종료 Ctrl + ] *** 13. VM 정보 확인 VM 정보 virsh dominfo ubuntu24 CPU 정보 virsh vcpuinfo ubuntu24 메모리 정보 virsh dommemstat ubuntu24 XML 설정 확인 virsh dumpxml ubuntu24 *** 14. 디렉터리 구조 /var/lib/libvirt/ ├── images/ │ ├── ubuntu24.qcow2 │ └── ubuntu-24.04.iso ├── dnsmasq/ ├── network/ └── storage/ *** 실무에서 권장하는 구성 *** 마무리 KVM은 Linux 커널에 내장된 강력한 가상화 기술이며, libvirt를 함께 사용하면 CLI와 GUI 환경 모두에서 효율적으로 가상머신을 관리할 수 있습니다. 특히 Ubuntu Server 환경에서는 "virsh"와 "virt-install"을 이용한 VM 생성 방식이 가장 널리 사용되며, 운영 환경에서는 Bridge 네트워크와 qcow2 디스크 포맷을 함께 사용하는 구성이 일반적입니다.

August 4, 2026
Docker와 KVM의 차이점 알아보기카테고리

Docker와 KVM의 차이점 알아보기

Docker와 KVM의 차이점 알아보기 가상화 기술을 공부하다 보면 가장 많이 접하게 되는 것이 Docker와 **KVM(Kernel-based Virtual Machine)**입니다. 처음에는 둘 다 가상화 기술이라 비슷해 보이지만, 실제로는 동작 방식과 사용 목적이 크게 다릅니다. 이번 글에서는 Docker와 KVM의 차이점을 쉽게 이해할 수 있도록 정리해보겠습니다. *** Docker란? Docker는 컨테이너(Container) 기반 가상화 기술입니다. 애플리케이션을 실행하는 데 필요한 라이브러리와 설정 파일만 함께 묶어 실행하며, 호스트 운영체제의 커널을 공유합니다. 즉, 운영체제를 새로 설치하지 않고 애플리케이션만 독립적으로 실행하는 기술입니다. 예를 들어 하나의 Linux 서버에서 다음과 같이 여러 서비스를 동시에 실행할 수 있습니다. Linux Host │ ├── Docker │ ├── Nginx │ ├── MariaDB │ ├── Redis │ └── Spring Boot 각 컨테이너는 서로 독립적으로 동작하지만 모두 동일한 Linux 커널을 사용합니다. *** KVM이란? KVM(Kernel-based Virtual Machine)은 리눅스 커널에 포함된 하이퍼바이저 기반의 가상화 기술입니다. 하나의 물리 서버에서 여러 개의 독립적인 운영체제를 실행할 수 있습니다. 예를 들어 다음과 같이 구성할 수 있습니다. 물리 서버 │ ├── Ubuntu VM ├── Rocky Linux VM ├── Windows Server VM └── Debian VM 각 VM은 자체 운영체제와 커널을 가지고 있으므로 실제 서버와 거의 동일하게 동작합니다. *** Docker와 KVM의 가장 큰 차이 Docker는 애플리케이션을 가상화합니다. KVM은 운영체제 자체를 가상화합니다. 이 차이가 두 기술의 가장 큰 차이입니다. *** 구조 비교 Docker Linux │ └── Docker ├── Container 1 ├── Container 2 └── Container 3 * 하나의 Linux 커널을 공유합니다. * 컨테이너마다 필요한 라이브러리만 포함합니다. * 매우 가볍고 실행 속도가 빠릅니다. *** KVM Linux │ └── KVM ├── Ubuntu VM ├── Windows VM └── Rocky VM 각 VM에는 다음과 같은 요소가 모두 존재합니다. * Kernel * Driver * System Library * System Service 즉, 각각 하나의 독립적인 컴퓨터라고 생각하면 이해하기 쉽습니다. *** Docker와 KVM 비교 *** 메모리 사용량 비교 Docker는 운영체제를 따로 실행하지 않기 때문에 메모리 사용량이 매우 적습니다. 예를 들어 Docker Spring Boot Redis MariaDB → 총 1~2GB 정도 반면 KVM은 운영체제를 각각 실행해야 하므로 메모리를 많이 사용합니다. Ubuntu VM RAM 4GB Windows VM RAM 8GB Rocky VM RAM 4GB VM마다 별도로 메모리를 할당해야 합니다. *** 실행 속도 비교 Docker는 컨테이너만 실행하면 되므로 매우 빠릅니다. docker compose up -d 몇 초 안에 실행됩니다. 반면 KVM은 운영체제를 부팅해야 하므로 시간이 더 오래 걸립니다. *** 실제 사용 사례 Docker 다음과 같은 서비스 배포에 많이 사용됩니다. * Spring Boot * Node.js * Redis * MariaDB * MySQL * Nginx 예를 들어 services: nginx: spring: mariadb: 와 같이 Docker Compose 하나만으로 전체 서비스를 실행할 수 있습니다. *** KVM 다음과 같은 경우 많이 사용됩니다. * Windows 서버 운영 * 테스트 서버 여러 대 구성 * 개발 서버 분리 * 운영 서버 분리 * 클라우드 인프라 구축 예를 들어 물리 서버 ↓ Ubuntu (Web) ↓ Rocky (DB) ↓ Windows (업무 프로그램) 처럼 하나의 서버를 여러 대의 서버처럼 사용할 수 있습니다. *** Docker와 KVM을 함께 사용할 수 있을까? 가능합니다. 실제로 기업에서는 다음과 같은 구조를 가장 많이 사용합니다. 물리 서버 ↓ KVM ↓ Ubuntu VM ↓ Docker ↓ Nginx Spring Boot Redis MariaDB 즉, * KVM으로 서버를 분리하고 * Docker로 애플리케이션을 배포합니다. 이 구조는 클라우드 환경에서도 매우 많이 사용됩니다. *** 언제 Docker를 사용할까? 다음과 같은 경우 Docker를 사용하는 것이 좋습니다. * 웹 서비스 개발 * API 서버 개발 * CI/CD * MSA(Microservice) * 개발 환경 구축 * 빠른 배포 *** 언제 KVM을 사용할까? 다음과 같은 경우 KVM을 사용하는 것이 좋습니다. * Windows 서버가 필요한 경우 * 운영체제를 여러 개 실행해야 하는 경우 * 서버를 완전히 분리해야 하는 경우 * 테스트 서버를 여러 대 운영해야 하는 경우 *** 정리 Docker와 KVM은 경쟁 관계가 아니라 서로 다른 목적을 가진 기술입니다. Docker는 애플리케이션을 실행하기 위한 컨테이너 기술이며, KVM은 운영체제를 가상화하는 서버 가상화 기술입니다. 최근에는 대부분의 기업에서 KVM 위에 Docker를 설치하여 사용하는 구조를 채택하고 있습니다. 즉, KVM으로 서버를 구성하고 Docker로 서비스를 배포하는 방식이 가장 일반적인 운영 형태입니다.

August 3, 2026
Prometheus, Grafana, Node Exporter 모니터링 환경 구축카테고리

Prometheus, Grafana, Node Exporter 모니터링 환경 구축

Podman Compose로 Prometheus, Grafana, Node Exporter 모니터링 환경 구축하기 서버를 운영하다 보면 다음과 같은 정보를 계속 확인해야 합니다. * CPU 사용률이 갑자기 높아지지 않았는지 * 메모리가 부족하지 않은지 * 디스크 용량이 가득 차지 않았는지 * 네트워크 송수신량이 비정상적으로 증가하지 않았는지 * 모니터링 대상 서버가 정상적으로 살아 있는지 명령어로 서버에 접속하여 "top", "free", "df", "ss" 등을 실행하면 현재 상태를 확인할 수 있습니다. 하지만 과거 사용량 변화와 장애 발생 시점을 확인하거나 여러 서버를 한 화면에서 관리하기에는 불편합니다. 이때 많이 사용하는 조합이 다음 세 가지입니다. * Node Exporter: 리눅스 서버의 시스템 상태를 수집 가능한 형태로 제공 * Prometheus: Node Exporter의 메트릭을 주기적으로 수집하고 저장 * Grafana: Prometheus에 저장된 메트릭을 대시보드와 그래프로 시각화 이번 글에서는 Podman Compose를 사용하여 세 서비스를 한 번에 구성하고, 리눅스 서버의 CPU, 메모리, 디스크 및 네트워크 상태를 확인하는 방법을 정리합니다. *** 1. 전체 구성 이해하기 전체 흐름은 다음과 같습니다. ┌──────────────────────────────────────────┐ │ 리눅스 호스트 서버 │ │ │ │ Node Exporter │ │ └─ CPU, 메모리, 디스크, 네트워크 메트릭 │ └───────────────────┬──────────────────────┘ │ 9100/metrics 수집 ▼ ┌──────────────────────────────────────────┐ │ Prometheus │ │ └─ 메트릭 주기적 수집 및 시계열 저장 │ └───────────────────┬──────────────────────┘ │ PromQL 조회 ▼ ┌──────────────────────────────────────────┐ │ Grafana │ │ └─ 차트, 게이지, 표, 대시보드 시각화 │ └──────────────────────────────────────────┘ 각 서비스의 기본 포트는 다음과 같습니다. *** 2. Prometheus, Grafana, Node Exporter 역할 2.1 Node Exporter Node Exporter는 리눅스 서버의 운영체제 및 하드웨어 관련 메트릭을 "/metrics" 형식으로 제공합니다. 대표적으로 다음 항목을 수집할 수 있습니다. * CPU 사용 시간과 Load Average * 전체 메모리와 사용 가능 메모리 * Swap 사용량 * 파일 시스템 전체 용량과 여유 공간 * 디스크 읽기 및 쓰기 * 네트워크 송수신량 * 네트워크 오류 및 드롭 패킷 * 서버 부팅 시간 * 파일 디스크립터와 커널 관련 정보 Node Exporter 자체가 데이터를 장기간 저장하는 것은 아닙니다. 현재 시스템 상태를 Prometheus가 읽을 수 있는 형태로 노출하는 역할을 합니다. 2.2 Prometheus Prometheus는 Node Exporter의 "/metrics" 주소에 일정한 간격으로 접속하여 메트릭을 가져옵니다. 수집한 데이터는 시간 정보와 함께 저장되므로 다음과 같은 분석이 가능합니다. * 최근 5분간 CPU 평균 사용률 * 지난 24시간 메모리 사용량 변화 * 일주일 동안 디스크 사용량 증가 추세 * 특정 시점에 서버가 응답하지 않았는지 확인 * 네트워크 트래픽 급증 시점 확인 Prometheus 데이터는 PromQL이라는 전용 쿼리 언어로 조회합니다. 2.3 Grafana Grafana는 Prometheus를 데이터 소스로 연결하여 수집된 데이터를 시각화합니다. 다음과 같은 형태로 표현할 수 있습니다. * CPU 사용률 선 그래프 * 메모리 사용률 게이지 * 디스크 사용률 표 * 네트워크 송수신량 차트 * 서버 정상 여부 상태 패널 * 임계치 초과 알림 즉, Node Exporter가 측정값을 제공하고, Prometheus가 저장하며, Grafana가 화면으로 보여주는 구조입니다. *** 3. 디렉터리 구성 작업할 디렉터리를 생성합니다. mkdir -p monitoring cd monitoring 최종 디렉터리 구조는 다음과 같습니다. monitoring/ ├── docker-compose.yml └── prometheus.yml Podman Compose 환경에 따라 파일명을 "compose.yml" 또는 "compose.yaml"로 사용해도 됩니다. *** 4. Docker Compose 파일 작성 "docker-compose.yml" 파일을 생성합니다. version: '3.8' services: node-exporter: image: prom/node-exporter:latest container_name: node-exporter restart: always network_mode: "host" pid: "host" volumes: - /:/host:ro,rslave command: - '--path.rootfs=/host' prometheus: image: prom/prometheus:latest container_name: prometheus restart: always ports: - "9090:9090" volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:Z - prometheus-data:/prometheus grafana: image: grafana/grafana:latest container_name: grafana restart: always ports: - "3000:3000" volumes: - grafana-data:/var/lib/grafana:Z volumes: prometheus-data: grafana-data: «최신 Docker Compose에서는 최상단의 "version" 항목이 사실상 필요하지 않습니다. 기존 Compose 및 Podman Compose 환경과의 호환성을 고려하여 그대로 사용할 수 있으며, 경고가 표시되는 경우 "version: '3.8'" 줄만 제거해도 됩니다.» *** 5. Compose 설정 상세 설명 5.1 Node Exporter 설정 node-exporter: image: prom/node-exporter:latest container_name: node-exporter restart: always network_mode: "host" pid: "host" volumes: - /:/host:ro,rslave command: - '--path.rootfs=/host' "network_mode: "host"" Node Exporter가 컨테이너의 네트워크가 아니라 호스트의 네트워크 환경을 기준으로 동작하도록 설정합니다. 이 설정을 사용하면 별도의 다음 포트 매핑은 필요하지 않습니다. ports: - "9100:9100" Node Exporter는 호스트의 "9100" 포트에서 직접 실행됩니다. "pid: "host"" Node Exporter가 호스트의 PID 네임스페이스를 사용하도록 설정합니다. 컨테이너 내부 프로세스가 아니라 호스트 시스템 기준으로 일부 정보를 확인하기 위한 설정입니다. "/:/host:ro,rslave" 호스트의 루트 파일 시스템 "/"을 컨테이너의 "/host"에 읽기 전용으로 연결합니다. 옵션의 의미는 다음과 같습니다. * "ro": 읽기 전용 마운트 * "rslave": 호스트의 하위 마운트 변경 사항을 컨테이너에서도 확인 Node Exporter는 호스트 시스템을 모니터링해야 하므로 컨테이너 내부 파일 시스템이 아닌 실제 호스트 파일 시스템을 참조해야 합니다. «"/" 전체를 마운트하는 항목에는 ":Z"를 추가하지 않는 것이 좋습니다. 시스템 전체 디렉터리에 SELinux 재라벨링을 적용하면 다른 서비스에 영향을 줄 수 있습니다.» "--path.rootfs=/host" Node Exporter가 "/host"를 호스트의 루트 파일 시스템으로 인식하도록 지정합니다. 이 옵션이 없으면 Node Exporter가 호스트가 아닌 컨테이너 파일 시스템을 기준으로 일부 메트릭을 수집할 수 있습니다. *** 5.2 Prometheus 설정 prometheus: image: prom/prometheus:latest container_name: prometheus restart: always ports: - "9090:9090" volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:Z - prometheus-data:/prometheus "9090:9090" 호스트의 "9090" 포트를 Prometheus 컨테이너의 "9090" 포트에 연결합니다. 브라우저에서는 다음 주소로 접속합니다. http://서버-IP:9090 "prometheus.yml" 마운트 - ./prometheus.yml:/etc/prometheus/prometheus.yml:Z 현재 디렉터리의 "prometheus.yml" 파일을 컨테이너 내부의 Prometheus 설정 파일 위치에 연결합니다. Podman과 SELinux를 사용하는 환경에서는 ":Z" 옵션으로 해당 파일을 현재 컨테이너가 접근할 수 있도록 재라벨링합니다. Prometheus 데이터 볼륨 - prometheus-data:/prometheus Prometheus가 수집한 시계열 데이터를 Named Volume에 저장합니다. 이 볼륨이 없으면 컨테이너 삭제 후 기존 모니터링 데이터가 함께 사라질 수 있습니다. *** 5.3 Grafana 설정 grafana: image: grafana/grafana:latest container_name: grafana restart: always ports: - "3000:3000" volumes: - grafana-data:/var/lib/grafana:Z "3000:3000" 호스트의 "3000" 포트를 Grafana 컨테이너의 "3000" 포트에 연결합니다. 브라우저에서는 다음 주소로 접속합니다. http://서버-IP:3000 Grafana 데이터 볼륨 - grafana-data:/var/lib/grafana:Z Grafana는 다음 정보를 "/var/lib/grafana"에 저장합니다. * 사용자 계정 * 데이터 소스 설정 * 대시보드 * 폴더 * 알림 설정 * 플러그인 및 내부 데이터 Named Volume을 연결하면 컨테이너를 다시 생성해도 설정과 대시보드가 유지됩니다. *** 6. Prometheus 수집 설정 작성 "prometheus.yml" 파일을 생성합니다. global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: "prometheus" static_configs: - targets: - "localhost:9090" - job_name: "node-exporter" static_configs: - targets: - "host.containers.internal:9100" "scrape_interval" scrape_interval: 15s Prometheus가 각 모니터링 대상에서 메트릭을 가져오는 기본 주기입니다. 위 설정에서는 15초마다 Node Exporter의 메트릭을 수집합니다. Prometheus 자체 모니터링 - job_name: "prometheus" static_configs: - targets: - "localhost:9090" Prometheus가 자기 자신의 상태를 수집합니다. 이때 "localhost:9090"은 Prometheus 컨테이너 내부의 Prometheus 자신을 의미하므로 사용할 수 있습니다. Node Exporter 모니터링 - job_name: "node-exporter" static_configs: - targets: - "host.containers.internal:9100" Node Exporter는 "network_mode: host"로 실행되고, Prometheus는 기본 컨테이너 네트워크에서 실행됩니다. 따라서 Prometheus 설정에서 다음과 같이 작성하면 안 됩니다. targets: - "localhost:9100" Prometheus 컨테이너에서 "localhost"는 호스트 서버가 아니라 Prometheus 컨테이너 자신을 가리키기 때문입니다. Podman은 일반적으로 컨테이너에서 호스트로 접근할 수 있도록 다음 호스트명을 제공합니다. host.containers.internal 환경에 따라 이 이름이 동작하지 않는 경우에는 호스트 서버의 실제 IP를 사용합니다. - job_name: "node-exporter" static_configs: - targets: - "192.168.0.10:9100" "192.168.0.10" 부분은 실제 서버 IP로 변경해야 합니다. *** 7. 컨테이너 실행 Compose 파일이 있는 디렉터리에서 실행합니다. podman compose up -d 환경에 따라 다음 명령어를 사용해야 할 수도 있습니다. podman-compose up -d "-d" 옵션은 컨테이너를 백그라운드에서 실행한다는 의미입니다. 실행 상태를 확인합니다. podman ps 정상적으로 실행되면 다음 세 컨테이너가 표시됩니다. node-exporter prometheus grafana *** 8. Node Exporter 확인 서버에서 Node Exporter 메트릭 주소를 확인합니다. curl http://127.0.0.1:9100/metrics 정상이라면 다음과 같은 메트릭이 출력됩니다. node_cpu_seconds_total{cpu="0",mode="idle"} 12345.67 node_memory_MemTotal_bytes 1.6651070464e+10 node_memory_MemAvailable_bytes 9.834479616e+09 node_filesystem_size_bytes{device="/dev/mapper/root"} 1.07294887936e+11 출력량이 많으므로 처음 몇 줄만 확인하려면 다음 명령어를 사용합니다. curl -s http://127.0.0.1:9100/metrics | head <br /> *** 9. Prometheus 확인 브라우저에서 다음 주소로 접속합니다. http://서버-IP:9090 Prometheus 메뉴에서 다음 경로로 이동합니다. Status → Targets 다음 두 대상이 "UP" 상태여야 합니다. Prometheus 쿼리 화면에서 다음 쿼리를 실행할 수도 있습니다. up 결과가 "1"이면 해당 대상에서 메트릭을 정상적으로 수집하고 있다는 의미입니다. up{job="prometheus"} 1 up{job="node-exporter"} 1 결과가 "0"이면 대상이 등록되어 있지만 현재 수집에 실패한 상태입니다. <br /> *** 10. Grafana 접속 브라우저에서 다음 주소로 접속합니다. http://서버-IP:3000 초기 관리자 계정은 기본 설정 기준으로 다음과 같습니다. 아이디: admin 비밀번호: admin 처음 로그인한 뒤에는 관리자 비밀번호를 반드시 변경합니다. 운영 환경에서는 Compose 파일에 초기 관리자 계정을 환경 변수로 설정할 수도 있습니다. grafana: image: grafana/grafana:latest container_name: grafana restart: always environment: GF_SECURITY_ADMIN_USER: admin GF_SECURITY_ADMIN_PASSWORD: 변경할-강력한-비밀번호 ports: - "3000:3000" volumes: - grafana-data:/var/lib/grafana:Z 비밀번호를 Git 저장소에 그대로 저장하는 방식은 권장하지 않습니다. 실제 운영 환경에서는 환경 파일, Secret 또는 별도의 비밀정보 관리 방법을 사용하는 것이 좋습니다. <br />*** 11. Grafana에 Prometheus 연결 Grafana 로그인 후 다음 메뉴로 이동합니다. Connections → Data sources → Add new data source "Prometheus"를 선택하고 서버 URL에 다음 값을 입력합니다. http://prometheus:9090 Grafana와 Prometheus는 같은 Compose 프로젝트의 기본 네트워크에 연결되므로 서비스 이름인 "prometheus"로 접근할 수 있습니다. 다음 주소를 입력하면 안 됩니다. http://localhost:9090 Grafana 컨테이너에서 "localhost"는 Grafana 컨테이너 자신을 의미하기 때문입니다. 마지막으로 "Save & test"를 실행하여 연결 성공 여부를 확인합니다. *** 12. Grafana 대시보드 만들기 Grafana에서는 직접 패널을 만들거나 기존 대시보드 JSON을 가져올 수 있습니다. 직접 대시보드를 만들려면 다음 메뉴로 이동합니다. Dashboards → New → New dashboard 패널을 추가한 후 데이터 소스로 Prometheus를 선택하고 PromQL을 입력합니다. 12.1 서버 정상 여부 up{job="node-exporter"} * "1": 정상 * "0": 수집 실패 또는 서버 응답 없음 12.2 CPU 사용률 100 - ( avg by (instance) ( rate(node_cpu_seconds_total{mode="idle"}[5m]) ) * 100 ) 최근 5분간 CPU 유휴 시간을 기준으로 전체 CPU 사용률을 계산합니다. 12.3 메모리 사용률 ( 1 - ( node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes ) ) * 100 전체 메모리 중 현재 사용 중인 메모리 비율을 계산합니다. 12.4 파일 시스템 사용률 100 - ( node_filesystem_avail_bytes{ fstype!~"tmpfs|overlay|squashfs" } / node_filesystem_size_bytes{ fstype!~"tmpfs|overlay|squashfs" } * 100 ) 임시 파일 시스템과 컨테이너 Overlay 파일 시스템을 제외한 디스크 사용률을 확인합니다. 마운트 위치별로 구분하려면 Grafana 범례에 다음 값을 사용할 수 있습니다. {{instance}} - {{mountpoint}} 12.5 네트워크 수신량 rate(node_network_receive_bytes_total{device!="lo"}[5m]) Loopback 인터페이스를 제외한 초당 네트워크 수신 바이트를 확인합니다. 12.6 네트워크 송신량 rate(node_network_transmit_bytes_total{device!="lo"}[5m]) 초당 네트워크 송신 바이트를 확인합니다. 12.7 서버 부하 node_load1 1분 Load Average를 확인합니다. 다음 메트릭도 함께 사용할 수 있습니다. node_load5 node_load15 <br />

July 28, 2026
Milkdown 에디터 사용법 정리카테고리

Milkdown 에디터 사용법 정리

마크다운 기반 WYSIWYG 에디터를 프로젝트에 적용하는 방법 웹 서비스를 개발하다 보면 게시글 작성, 공지사항 작성, 매뉴얼 작성, 위키 문서 작성처럼 사용자가 긴 글을 입력해야 하는 기능이 필요합니다. 이때 단순한 "<textarea>"만으로는 편집 기능이 부족하고, 일반적인 리치 텍스트 에디터를 사용하면 데이터 저장 방식이나 마크다운 관리가 복잡해질 수 있습니다. 이러한 상황에서 사용할 수 있는 에디터 중 하나가 Milkdown입니다. Milkdown은 마크다운을 기반으로 동작하는 WYSIWYG 에디터 프레임워크입니다. WYSIWYG는 “What You See Is What You Get”의 약자로, 사용자가 화면에서 보는 형태 그대로 결과물이 만들어지는 편집 방식을 의미합니다. Milkdown은 사용자가 보기 좋은 편집 화면에서 글을 작성할 수 있도록 지원하면서도, 개발자는 결과 데이터를 마크다운 형태로 저장하고 관리할 수 있도록 도와주는 에디터입니다. 1. Milkdown이란 무엇인가요? Milkdown은 웹 프로젝트에 적용할 수 있는 마크다운 중심의 에디터 프레임워크입니다. 일반적인 리치 텍스트 에디터는 HTML 중심으로 동작하는 경우가 많습니다. 사용자가 글을 작성하면 내부 데이터가 HTML이 되거나, 별도의 JSON 구조로 저장되는 경우도 있습니다. 반면 Milkdown은 마크다운을 핵심 데이터로 다룹니다. 즉, 사용자는 편집 화면에서 보기 좋게 글을 작성하고, 개발자는 작성된 내용을 마크다운 문자열로 저장할 수 있습니다. Milkdown은 내부적으로 ProseMirror와 Remark를 기반으로 동작합니다. ProseMirror는 웹 기반 문서 편집기를 만들기 위한 프레임워크이며, Remark는 마크다운을 처리하기 위한 도구입니다. Milkdown은 이 두 가지 구조를 바탕으로 마크다운 기반의 편집 경험을 제공합니다. 2. Milkdown을 사용하는 이유 Milkdown을 사용하는 이유는 여러 가지가 있습니다. 첫 번째 이유는 마크다운 기반 저장이 가능하다는 점입니다. 게시글, 기술 문서, 공지사항, 매뉴얼 같은 데이터는 HTML보다 마크다운으로 저장하는 것이 더 깔끔한 경우가 많습니다. 마크다운은 사람이 읽기 쉽고, 다른 플랫폼으로 옮기거나 HTML로 변환하기도 편리합니다. 두 번째 이유는 WYSIWYG 편집 경험을 제공한다는 점입니다. 사용자가 "# 제목", "**굵게**", "- 목록" 같은 마크다운 문법을 정확히 몰라도 편집 화면에서 자연스럽게 글을 작성할 수 있습니다. 개발자는 마크다운 데이터를 관리할 수 있고, 사용자는 일반 문서 편집기처럼 글을 작성할 수 있습니다. 세 번째 이유는 플러그인 기반 구조를 가진다는 점입니다. Milkdown은 필요한 기능을 플러그인 방식으로 추가할 수 있습니다. CommonMark, GFM, 히스토리, 클립보드, 리스너, 업로드, Slash 명령어, 툴팁, 테이블, 코드 블록 같은 기능을 프로젝트 상황에 맞게 조합할 수 있습니다. 네 번째 이유는 커스터마이징이 자유롭다는 점입니다. 완성형 에디터처럼 빠르게 붙여서 사용할 수도 있고, 프로젝트 요구사항에 맞게 직접 에디터 구성을 조립할 수도 있습니다. 관리자 화면, 블로그 작성 화면, 문서 관리 시스템처럼 다양한 형태의 화면에 적용하기 좋습니다. 다섯 번째 이유는 React, Vue, Svelte, Solid, Next.js, Nuxt 같은 프레임워크와 함께 사용할 수 있다는 점입니다. 프론트엔드 프레임워크를 사용하는 프로젝트에서도 Milkdown을 적용할 수 있으며, Vanilla TypeScript 환경에서도 사용할 수 있습니다. 3. Milkdown과 Crepe의 차이 Milkdown을 처음 사용할 때 헷갈릴 수 있는 개념이 있습니다. 바로 Milkdown과 Crepe의 차이입니다. 간단히 정리하면 다음과 같습니다. * Milkdown은 에디터를 만들기 위한 핵심 프레임워크입니다. * Crepe는 Milkdown 위에 만들어진 완성형 에디터입니다. Milkdown을 직접 사용하면 필요한 플러그인, 테마, 명령어, UI를 직접 조합해야 합니다. 반면 Crepe를 사용하면 기본 UI와 주요 기능이 포함된 에디터를 빠르게 적용할 수 있습니다. 처음 Milkdown을 프로젝트에 적용한다면 Crepe부터 사용하는 방식을 추천합니다. Crepe는 기본적인 편집 UI, 마크다운 작성 기능, 테마, 주요 편집 기능을 포함하고 있기 때문에 빠르게 결과물을 확인할 수 있습니다. 4. 설치 방법 가장 빠르게 시작하려면 "@milkdown/crepe"를 설치하면 됩니다. npm install @milkdown/crepe pnpm을 사용한다면 다음과 같이 설치합니다. pnpm add @milkdown/crepe yarn을 사용한다면 다음과 같이 설치합니다. yarn add @milkdown/crepe Crepe를 설치하면 Milkdown 기반의 완성형 마크다운 에디터를 바로 사용할 수 있습니다. 5. 기본 사용 예제 먼저 HTML에 에디터가 들어갈 영역을 생성합니다. <div id="app"></div> 그다음 TypeScript 또는 JavaScript 파일에서 Crepe 인스턴스를 생성합니다. import { Crepe } from '@milkdown/crepe' import '@milkdown/crepe/theme/common/style.css' import '@milkdown/crepe/theme/frame.css' const crepe = new Crepe({ root: '#app', defaultValue: '# Hello Milkdown\n\nMilkdown 에디터를 시작합니다.', }) await crepe.create() 위 코드를 실행하면 "#app" 영역에 Milkdown 기반 에디터가 생성됩니다. 여기서 중요한 설정은 세 가지입니다. 첫 번째는 "root"입니다. "root"는 에디터가 붙을 DOM 영역을 지정하는 옵션입니다. "'#app'"처럼 CSS 선택자를 사용할 수도 있고, "document.getElementById('app')"처럼 실제 DOM 객체를 넘길 수도 있습니다. 두 번째는 "defaultValue"입니다. "defaultValue"는 에디터가 처음 열릴 때 표시할 기본 마크다운 내용을 설정하는 옵션입니다. 세 번째는 CSS import입니다. Crepe는 테마 CSS를 import해야 화면이 정상적으로 표시됩니다. 공통 스타일을 먼저 import하고, 그다음 원하는 테마 CSS를 import하면 됩니다. 6. 테마 적용 방법 Crepe는 여러 가지 테마를 제공합니다. 대표적인 테마는 다음과 같습니다. * "frame" 테마입니다. * "crepe" 테마입니다. * "nord" 테마입니다. * "frame-dark" 테마입니다. * "crepe-dark" 테마입니다. * "nord-dark" 테마입니다. 테마를 적용할 때는 공통 스타일을 먼저 import하고, 그다음 사용할 테마 CSS를 import합니다. import '@milkdown/crepe/theme/common/style.css' import '@milkdown/crepe/theme/frame.css' 다크 테마를 사용하고 싶다면 다음과 같이 변경합니다. import '@milkdown/crepe/theme/common/style.css' import '@milkdown/crepe/theme/frame-dark.css' 관리자 페이지나 CMS 화면에서는 "frame" 또는 "nord" 계열 테마가 무난합니다. 다크 테마 기반의 대시보드나 관제 화면에 적용한다면 "frame-dark" 테마도 좋은 선택입니다. 7. 현재 작성된 마크다운 가져오기 에디터에서 작성된 내용을 DB에 저장하려면 현재 내용을 마크다운 문자열로 가져와야 합니다. Crepe에서는 "getMarkdown()" 메서드를 사용할 수 있습니다. const markdown = crepe.getMarkdown() console.log(markdown) 저장 버튼과 연결하면 다음과 같이 사용할 수 있습니다. const saveButton = document.getElementById('saveButton') saveButton?.addEventListener('click', async () => { const markdown = crepe.getMarkdown() await fetch('/api/posts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ title: '게시글 제목', content: markdown, }), }) }) 이 방식으로 사용자가 작성한 내용을 마크다운 문자열로 가져온 뒤 서버 API로 전송할 수 있습니다. 8. 내용 변경 이벤트 감지하기 에디터 내용이 변경될 때마다 특정 작업을 해야 하는 경우가 있습니다. 예를 들어 자동 저장, 글자 수 계산, 미리보기 갱신, 임시 저장 기능을 구현할 때 내용 변경 이벤트가 필요합니다. Crepe에서는 "crepe.on()"을 사용하여 리스너를 등록할 수 있습니다. crepe.on((listener) => { listener.markdownUpdated((ctx, markdown, prevMarkdown) => { console.log('현재 마크다운:', markdown) console.log('이전 마크다운:', prevMarkdown) }) listener.focus((ctx) => { console.log('에디터 포커스') }) listener.blur((ctx) => { console.log('에디터 블러') }) }) 자동 저장 기능은 다음과 같이 구현할 수 있습니다. let timer: ReturnType<typeof setTimeout> | null = null crepe.on((listener) => { listener.markdownUpdated((ctx, markdown) => { if (timer) { clearTimeout(timer) } timer = setTimeout(() => { localStorage.setItem('draft-content', markdown) console.log('임시 저장 완료') }, 500) }) }) 위 코드는 사용자가 글을 입력할 때마다 바로 저장하지 않고, 0.5초 동안 입력이 멈추면 임시 저장을 수행하는 방식입니다. 이러한 구조를 사용하면 서버 요청을 너무 자주 보내지 않으면서도 자동 저장 기능을 구현할 수 있습니다. 9. 읽기 전용 모드 적용하기 게시글 상세보기 화면에서는 사용자가 내용을 수정하지 못하도록 읽기 전용 모드를 적용해야 할 수 있습니다. Crepe에서는 "setReadonly()" 메서드를 사용할 수 있습니다. crepe.setReadonly(true) 다시 수정 가능한 상태로 변경하려면 다음과 같이 사용합니다. crepe.setReadonly(false) 읽기 전용 모드는 게시글 상세보기, 공지사항 상세보기, 매뉴얼 조회 화면 등에 사용할 수 있습니다. 10. 에디터 제거하기 SPA 환경에서는 화면을 이동하거나 컴포넌트가 사라질 때 에디터 인스턴스를 정리해야 합니다. 이때는 "destroy()" 메서드를 사용합니다. crepe.destroy() 에디터 인스턴스를 정리하지 않으면 이벤트 리스너나 DOM 참조가 남아 메모리 누수가 발생할 수 있습니다. React, Vue 같은 프레임워크에서는 컴포넌트가 unmount될 때 에디터를 정리하는 구조를 잡는 것이 좋습니다. 11. React에서 Milkdown 사용하기 React 프로젝트에서는 "@milkdown/react"를 함께 사용할 수 있습니다. 먼저 필요한 패키지를 설치합니다. npm install @milkdown/crepe @milkdown/react @milkdown/kit 기본 구조는 다음과 같습니다. import React from 'react' import { Crepe } from '@milkdown/crepe' import { Milkdown, MilkdownProvider, useEditor } from '@milkdown/react' import '@milkdown/crepe/theme/common/style.css' import '@milkdown/crepe/theme/frame.css' const CrepeEditor = () => { useEditor((root) => { return new Crepe({ root, defaultValue: '# 제목\n\n내용을 입력하세요.', }) }, []) return <Milkdown /> } export default function EditorPage() { return ( <MilkdownProvider> <CrepeEditor /> </MilkdownProvider> ) } React에서 Milkdown을 사용할 때는 "MilkdownProvider"로 에디터 영역을 감싸고, "useEditor()"를 사용하여 에디터 인스턴스를 생성합니다. "Milkdown" 컴포넌트는 실제 에디터가 렌더링되는 영역입니다. 12. React에서 저장 버튼 만들기 React에서 저장 버튼을 만들려면 에디터 인스턴스에 접근해야 합니다. "useInstance()"를 사용하면 현재 생성된 에디터 인스턴스를 가져올 수 있습니다. import React from 'react' import { Crepe } from '@milkdown/crepe' import { Milkdown, MilkdownProvider, useEditor, useInstance } from '@milkdown/react' import { getMarkdown } from '@milkdown/kit/utils' import '@milkdown/crepe/theme/common/style.css' import '@milkdown/crepe/theme/frame.css' const Editor = () => { useEditor((root) => { return new Crepe({ root, defaultValue: '# 게시글 제목\n\n내용을 입력하세요.', }) }, []) return <Milkdown /> } const EditorControls = () => { const [loading, getEditor] = useInstance() const handleSave = () => { if (loading) { return } const editor = getEditor() if (!editor) { return } const markdown = editor.action(getMarkdown()) console.log('저장할 마크다운:', markdown) } return ( <button type="button" onClick={handleSave}> 저장 </button> ) } export default function EditorPage() { return ( <MilkdownProvider> <Editor /> <EditorControls /> </MilkdownProvider> ) } 여기서 중요한 점은 "EditorControls" 컴포넌트가 반드시 "MilkdownProvider" 내부에 있어야 한다는 점입니다. 이 구조를 사용하면 React 화면에서 에디터와 저장 버튼을 분리해서 관리할 수 있습니다. 13. 기존 게시글 수정 화면 만들기 게시글 수정 화면에서는 서버에서 가져온 마크다운 내용을 에디터에 넣어야 합니다. 가장 단순한 방식은 "defaultValue"에 서버에서 조회한 내용을 넣는 방식입니다. const Editor = ({ content }: { content: string }) => { useEditor((root) => { return new Crepe({ root, defaultValue: content, }) }, [content]) return <Milkdown /> } 하지만 실제 프로젝트에서는 게시글 내용이 API 호출 이후 늦게 들어오는 경우가 많습니다. 이 경우 에디터가 이미 생성된 뒤에 내용을 교체해야 할 수 있습니다. Milkdown에서는 전체 내용을 교체할 때 "replaceAll" 매크로를 사용할 수 있습니다. import { replaceAll } from '@milkdown/kit/utils' const handleLoadContent = () => { const editor = getEditor() if (!editor) { return } editor.action(replaceAll('# 서버에서 가져온 제목\n\n서버 내용입니다.')) } 수정 화면의 일반적인 처리 흐름은 다음과 같습니다. 1. 게시글 상세 API를 호출합니다. 2. 서버에서 마크다운 내용을 가져옵니다. 3. 에디터를 생성합니다. 4. 조회된 마크다운을 에디터에 반영합니다. 5. 사용자가 내용을 수정합니다. 6. 저장 버튼 클릭 시 "getMarkdown()"으로 현재 내용을 가져옵니다. 7. 수정 API로 마크다운 내용을 전송합니다. 이 흐름으로 구성하면 등록 화면과 수정 화면을 동일한 에디터 구조로 관리할 수 있습니다. 14. "@milkdown/kit"으로 직접 에디터 만들기 Crepe는 바로 사용할 수 있는 완성형 에디터입니다. 하지만 기능을 더 세밀하게 제어하고 싶다면 "@milkdown/kit"을 사용하여 직접 에디터를 구성할 수 있습니다. 먼저 패키지를 설치합니다. npm install @milkdown/kit 기본 에디터는 다음과 같이 만들 수 있습니다. import { Editor } from '@milkdown/kit/core' import { commonmark } from '@milkdown/kit/preset/commonmark' import '@milkdown/kit/prose/view/style/prosemirror.css' const editor = await Editor.make() .use(commonmark) .create() 이 방식은 Crepe보다 초기 설정이 많지만, 필요한 기능만 선택하여 구성할 수 있다는 장점이 있습니다. 프로젝트에서 툴바, 명령어, 업로드, 미리보기, 단축키 등을 직접 제어해야 한다면 "@milkdown/kit" 기반으로 구성하는 방식이 적합합니다. 15. 히스토리 기능 추가하기 사용자가 글을 작성하다가 실행 취소와 다시 실행을 할 수 있어야 한다면 history 플러그인을 추가합니다. import { Editor } from '@milkdown/kit/core' import { commonmark } from '@milkdown/kit/preset/commonmark' import { history } from '@milkdown/kit/plugin/history' import { nord } from '@milkdown/theme-nord' import '@milkdown/theme-nord/style.css' const editor = await Editor.make() .config(nord) .use(commonmark) .use(history) .create() "history" 플러그인을 추가하면 사용자가 입력한 내용을 되돌리거나 다시 실행할 수 있습니다. 문서 작성 화면에서는 실행 취소 기능이 거의 필수이기 때문에 기본적으로 추가하는 것이 좋습니다. 16. 리스너 플러그인으로 자동 저장 구현하기 "@milkdown/kit"을 직접 사용할 때는 listener 플러그인을 붙여 내용 변경을 감지할 수 있습니다. import { Editor, rootCtx } from '@milkdown/kit/core' import { commonmark } from '@milkdown/kit/preset/commonmark' import { listener, listenerCtx } from '@milkdown/kit/plugin/listener' const editor = await Editor.make() .config((ctx) => { ctx.set(rootCtx, document.getElementById('app')) ctx.get(listenerCtx).markdownUpdated((ctx, markdown) => { console.log('변경된 마크다운:', markdown) localStorage.setItem('draft', markdown) }) }) .use(commonmark) .use(listener) .create() 이 구조를 활용하면 사용자가 글을 작성하는 동안 내용을 자동으로 임시 저장할 수 있습니다. 관리자 공지사항, 매뉴얼 작성, 블로그 작성처럼 긴 글을 작성하는 화면에서는 자동 저장 기능을 넣는 것이 좋습니다. 17. 코드 하이라이팅 적용하기 기술 블로그나 개발 문서에서는 코드 블록 하이라이팅이 중요합니다. Milkdown에서는 코드 블록 하이라이팅을 위한 플러그인을 사용할 수 있습니다. 설치 예시는 다음과 같습니다. npm install @milkdown/plugin-highlight Shiki를 사용하는 예시는 다음과 같습니다. import { Editor } from '@milkdown/core' import { commonmark } from '@milkdown/preset-commonmark' import { highlight, highlightPluginConfig } from '@milkdown/plugin-highlight' import { createParser } from '@milkdown/plugin-highlight/shiki' async function createEditor() { const parser = await createParser({ theme: 'github-light', langs: ['javascript', 'typescript', 'python', 'html', 'css', 'json'], }) const editor = await Editor.make() .config((ctx) => { ctx.set(highlightPluginConfig.key, { parser, }) }) .use(commonmark) .use(highlight) .create() return editor } 이렇게 설정하면 다음과 같은 코드 블록에 문법 강조가 적용됩니다. ```typescript const message: string = 'Hello Milkdown' console.log(message) ``` 개발 블로그나 기술 문서 관리 시스템을 만든다면 코드 하이라이팅 기능은 꼭 고려하는 것이 좋습니다. 18. 명령어 사용하기 Milkdown은 명령어 시스템을 제공합니다. 명령어를 사용하면 버튼 클릭 시 선택한 텍스트를 굵게 처리하거나, 제목으로 변경하거나, 목록을 삽입하는 기능을 만들 수 있습니다. 예를 들어 강조 명령을 실행하는 코드는 다음과 같습니다. import { Editor, commandsCtx } from '@milkdown/kit/core' import { commonmark, toggleEmphasisCommand, } from '@milkdown/kit/preset/commonmark' const editor = await Editor.make() .use(commonmark) .create() const toggleItalic = () => { editor.action((ctx) => { const commandManager = ctx.get(commandsCtx) commandManager.call(toggleEmphasisCommand.key) }) } 버튼과 연결하면 다음과 같이 사용할 수 있습니다. <button id="italicButton">기울임</button> document.getElementById('italicButton')?.addEventListener('click', () => { toggleItalic() }) 이 구조를 활용하면 프로젝트에 맞는 커스텀 툴바를 만들 수 있습니다. 예를 들어 관리자 화면에서 제목, 굵게, 목록, 코드 블록, 이미지 삽입 버튼만 제공하는 간단한 툴바를 직접 구성할 수 있습니다. 19. 매크로 사용하기 Milkdown에는 에디터를 쉽게 조작할 수 있는 매크로가 있습니다. 자주 사용하는 매크로는 다음과 같습니다. 19-1. 현재 커서 위치에 내용 삽입하기 import { insert } from '@milkdown/kit/utils' editor.action(insert('## 새 제목')) 현재 커서 위치에 원하는 마크다운 내용을 삽입할 수 있습니다. 19-2. 전체 내용 교체하기 import { replaceAll } from '@milkdown/kit/utils' editor.action(replaceAll('# 새 문서\n\n내용을 다시 작성합니다.')) 기존 내용을 모두 지우고 새로운 마크다운 내용으로 교체할 수 있습니다. 19-3. 현재 내용을 마크다운으로 가져오기 import { getMarkdown } from '@milkdown/kit/utils' const markdown = editor.action(getMarkdown()) 현재 에디터 내용을 마크다운 문자열로 가져올 수 있습니다. 19-4. 현재 내용을 HTML로 가져오기 import { getHTML } from '@milkdown/kit/utils' const html = editor.action(getHTML()) 현재 에디터 내용을 HTML 문자열로 가져올 수 있습니다. 다만 HTML을 화면에 출력할 때는 XSS 보안 처리를 반드시 고려해야 합니다. 20. 이미지 업로드 처리 방법 실제 게시판이나 CMS에서는 이미지 업로드 기능이 필요합니다. Milkdown은 에디터 프레임워크이기 때문에 이미지 업로드 정책은 프로젝트에 맞게 별도로 설계해야 합니다. 일반적인 이미지 업로드 흐름은 다음과 같습니다. 1. 사용자가 이미지를 선택하거나 드래그 앤 드롭합니다. 2. 프론트엔드에서 이미지 파일을 서버 업로드 API로 전송합니다. 3. 서버는 파일을 저장하고 접근 가능한 URL을 반환합니다. 4. 프론트엔드는 반환받은 이미지 URL을 에디터에 삽입합니다. 5. 에디터에는 마크다운 이미지 문법으로 이미지가 표시됩니다. 예를 들어 서버에서 "/uploads/sample.png"라는 URL을 반환했다면 다음과 같은 마크다운을 에디터에 넣을 수 있습니다. ![이미지 설명](/uploads/sample.png) 삽입 코드는 다음과 같이 작성할 수 있습니다. import { insert } from '@milkdown/kit/utils' const imageUrl = '/uploads/sample.png' editor.action(insert(`![이미지 설명](${imageUrl})`)) 관리자 페이지에서 이미지 업로드 기능을 구현할 때는 다음 항목을 함께 고려해야 합니다. * 파일 확장자 제한이 필요합니다. * 파일 크기 제한이 필요합니다. * 이미지 MIME 타입 검증이 필요합니다. * 저장 경로 관리가 필요합니다. * 원본 파일명과 저장 파일명을 분리해야 합니다. * XSS 방지를 위한 URL 검증이 필요합니다. * 게시글 삭제 시 첨부 이미지 정리 정책이 필요합니다. * 사용되지 않는 고아 파일 정리 정책이 필요합니다. 이미지 업로드는 단순히 파일을 올리는 기능이 아니라, 저장 정책과 보안 정책까지 함께 설계해야 하는 기능입니다. 21. DB에는 무엇을 저장해야 하나요? Milkdown을 게시판에 적용한다면 보통 DB에는 마크다운 원문을 저장합니다. 예를 들어 게시글 테이블은 다음과 같이 구성할 수 있습니다. CREATE TABLE board_post ( post_id BIGSERIAL PRIMARY KEY, title VARCHAR(200) NOT NULL, content_markdown TEXT NOT NULL, content_html TEXT, created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); 여기서 핵심 컬럼은 "content_markdown"입니다. Milkdown에서 가져온 마크다운 원문을 "content_markdown"에 저장합니다. "content_html"은 선택 사항입니다. HTML을 매번 렌더링할 때 변환해도 되고, 조회 성능이 중요하다면 저장 시점에 HTML로 변환해서 함께 저장할 수도 있습니다. 다만 HTML을 저장하거나 화면에 출력할 때는 반드시 XSS 처리를 고려해야 합니다. 개인적으로는 원본 데이터는 마크다운으로 저장하고, 화면 출력 시 필요한 경우에만 HTML로 변환하는 방식을 추천합니다. 22. Spring Boot API 예시 프론트에서 Milkdown으로 작성한 마크다운을 Spring Boot API로 저장하는 예시는 다음과 같습니다. @RestController @RequestMapping("/api/posts") @RequiredArgsConstructor public class PostController { private final PostService postService; @PostMapping public ResponseEntity<Long> createPost(@RequestBody PostCreateRequest request) { Long postId = postService.createPost(request); return ResponseEntity.ok(postId); } } 요청 DTO는 다음과 같이 작성할 수 있습니다. @Getter @Setter public class PostCreateRequest { private String title; private String contentMarkdown; } 서비스에서는 제목과 내용을 검증한 뒤 저장합니다. @Service @RequiredArgsConstructor public class PostService { private final PostMapper postMapper; @Transactional public Long createPost(PostCreateRequest request) { if (request.getTitle() == null || request.getTitle().isBlank()) { throw new IllegalArgumentException("제목은 필수입니다."); } if (request.getContentMarkdown() == null || request.getContentMarkdown().isBlank()) { throw new IllegalArgumentException("내용은 필수입니다."); } Post post = new Post(); post.setTitle(request.getTitle()); post.setContentMarkdown(request.getContentMarkdown()); postMapper.insertPost(post); return post.getPostId(); } } 프론트에서는 다음과 같이 전송할 수 있습니다. const markdown = crepe.getMarkdown() await fetch('/api/posts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ title: titleInput.value, contentMarkdown: markdown, }), }) 이 구조는 일반적인 게시글 등록, 공지사항 등록, 매뉴얼 등록 화면에 적용할 수 있습니다. 23. 게시글 상세보기에서는 어떻게 보여주나요? 게시글 상세보기에서는 두 가지 방식으로 내용을 보여줄 수 있습니다. 첫 번째 방식은 Milkdown을 읽기 전용 모드로 사용하는 방식입니다. const crepe = new Crepe({ root: '#viewer', defaultValue: markdownFromServer, }) await crepe.create() crepe.setReadonly(true) 이 방식은 에디터에서 작성한 형태와 상세보기 화면의 표현을 최대한 비슷하게 유지할 수 있다는 장점이 있습니다. 두 번째 방식은 마크다운을 HTML로 변환해서 보여주는 방식입니다. 블로그나 문서 화면처럼 단순 조회가 많은 화면에서는 마크다운을 HTML로 변환해서 출력하는 방식도 괜찮습니다. 다만 HTML을 직접 출력할 경우 XSS 처리가 반드시 필요합니다. 관리자 화면의 상세보기나 수정 화면에서는 Milkdown의 readonly 모드를 사용하는 방식이 편리합니다. 일반 사용자에게 공개되는 블로그 화면에서는 마크다운을 HTML로 변환해서 보여주는 방식이 더 가볍게 동작할 수 있습니다. 참고 자료 * Milkdown GitHub: "https://github.com/Milkdown/milkdown" (https://github.com/Milkdown/milkdown) * Milkdown Examples: "https://github.com/Milkdown/examples" (https://github.com/Milkdown/examples) * Milkdown 공식 문서: "https://milkdown.dev" (https://milkdown.dev/)

July 3, 2026
Next.js란? React 개발자가 알아야 할 풀스택 프레임워크카테고리

Next.js란? React 개발자가 알아야 할 풀스택 프레임워크

Next.js란? React 개발자가 알아야 할 풀스택 프레임워크 React로 웹 서비스를 만들다 보면 자연스럽게 다음과 같은 고민이 생깁니다. - 페이지 라우팅은 어떻게 구성할까? - 검색엔진 최적화, 즉 SEO는 어떻게 처리할까? - 초기 로딩 속도를 더 빠르게 만들 수 없을까? - 프론트엔드 프로젝트 안에서 간단한 API도 같이 만들 수 없을까? - 서버 사이드 렌더링과 정적 페이지 생성을 쉽게 적용할 방법은 없을까? 이런 문제를 해결하기 위해 많이 사용하는 프레임워크가 Next.js입니다. Next.js는 React 기반의 웹 애플리케이션 프레임워크입니다. 단순히 화면을 만드는 React 라이브러리에서 한 단계 더 나아가, 라우팅, 서버 렌더링, 정적 페이지 생성, 이미지 최적화, API 처리, 배포 최적화까지 웹 서비스 개발에 필요한 기능을 함께 제공합니다. *** 목차 1. "Next.js를 사용하는 이유" (#1-nextjs를-사용하는-이유) 2. "Next.js의 핵심 개념" (#2-nextjs의-핵심-개념) 3. "Next.js의 렌더링 방식" (#3-nextjs의-렌더링-방식) 4. "Route Handlers로 API 만들기" (#4-route-handlers로-api-만들기) 5. "Metadata API로 SEO 설정하기" (#5-metadata-api로-seo-설정하기) 6. "Image Optimization" (#6-image-optimization) 7. "프로젝트 생성 방법" (#7-프로젝트-생성-방법) 8. "기본 폴더 구조 예시" (#8-기본-폴더-구조-예시) 9. "Next.js가 적합한 경우" (#9-nextjs가-적합한-경우) 10. "Next.js 사용 시 주의할 점" (#10-nextjs-사용-시-주의할-점) 11. "간단한 예제 페이지" (#11-간단한-예제-페이지) 12. "마무리" (#12-마무리) *** 1. Next.js를 사용하는 이유 React만으로도 웹 화면은 충분히 만들 수 있습니다. 하지만 실제 서비스를 만들다 보면 화면 구성 외에도 처리해야 할 일이 많습니다. 예를 들어 일반적인 React 프로젝트에서는 라우팅을 위해 "react-router-dom"을 별도로 설정해야 하고, SEO를 위해 메타 태그 관리 방식을 따로 고민해야 하며, 서버에서 데이터를 미리 가져오는 구조도 직접 설계해야 합니다. Next.js는 이런 부분들을 프레임워크 차원에서 제공합니다. 대표적인 장점은 다음과 같습니다. *** 2. Next.js의 핵심 개념 2.1 App Router 최근 Next.js 프로젝트에서는 "app" 디렉터리를 사용하는 App Router 방식이 많이 사용됩니다. App Router는 파일 시스템 기반 라우팅을 제공합니다. 즉, 폴더 구조가 곧 URL 구조가 됩니다. 예를 들어 다음과 같은 구조가 있다고 가정합니다. app ├─ page.tsx ├─ layout.tsx ├─ about │ └─ page.tsx └─ posts ├─ page.tsx └─ [id] └─ page.tsx 위 구조는 다음 경로로 연결됩니다. 이처럼 Next.js에서는 라우터 설정 파일을 따로 만들지 않아도 폴더 구조만으로 페이지를 구성할 수 있습니다. *** 2.2 Layout "layout.tsx"는 여러 페이지가 공통으로 사용하는 레이아웃을 정의하는 파일입니다. 예를 들어 헤더, 사이드바, 푸터처럼 여러 페이지에서 반복되는 UI를 "layout.tsx"에 작성할 수 있습니다. // app/layout.tsx import type { Metadata } from "next"; export const metadata: Metadata = { title: "My Next App", description: "Next.js로 만든 웹 서비스입니다.", }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="ko"> <body> <header>공통 헤더</header> <main>{children}</main> <footer>공통 푸터</footer> </body> </html> ); } 이 구조를 사용하면 모든 페이지에서 공통 UI를 재사용할 수 있고, 페이지별로 중복 코드를 줄일 수 있습니다. *** 2.3 Server Components와 Client Components Next.js의 App Router에서는 기본적으로 컴포넌트가 Server Component로 동작합니다. Server Component는 서버에서 실행되는 컴포넌트입니다. 데이터베이스 조회, 서버 API 호출, 파일 시스템 접근처럼 브라우저에서 직접 처리하면 안 되는 작업을 서버에서 안전하게 처리할 수 있습니다. // app/posts/page.tsx async function getPosts() { const res = await fetch("https://jsonplaceholder.typicode.com/posts"); return res.json(); } export default async function PostsPage() { const posts = await getPosts(); return ( <div> <h1>게시글 목록</h1> <ul> {posts.slice(0, 5).map((post: any) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); } 반대로 브라우저에서 상태 관리, 클릭 이벤트, 입력값 처리 등이 필요한 컴포넌트는 Client Component로 만들어야 합니다. Client Component를 만들 때는 파일 상단에 ""use client""를 작성합니다. // app/components/Counter.tsx "use client"; import { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)}> 클릭 수: {count} </button> ); } 정리하면 다음과 같습니다. *** 3. Next.js의 렌더링 방식 Next.js를 이해할 때 가장 중요한 개념 중 하나는 렌더링 방식입니다. Next.js는 페이지 성격에 따라 여러 렌더링 방식을 선택할 수 있습니다. *** 3.1 SSR: Server Side Rendering SSR은 사용자가 페이지에 접근할 때마다 서버에서 HTML을 생성하는 방식입니다. 사용자별 데이터가 다르거나, 항상 최신 데이터를 보여줘야 하는 페이지에 적합합니다. 예를 들면 다음과 같은 화면에 사용할 수 있습니다. - 관리자 대시보드 - 로그인 사용자별 마이페이지 - 실시간 데이터 조회 화면 - 권한에 따라 내용이 달라지는 페이지 장점은 최신 데이터를 보여주기 좋고 SEO에도 유리하다는 점입니다. 단점은 요청마다 서버에서 HTML을 만들어야 하므로 서버 부하가 생길 수 있다는 점입니다. *** 3.2 SSG: Static Site Generation SSG는 빌드 시점에 HTML을 미리 생성해두는 방식입니다. 내용이 자주 바뀌지 않는 페이지에 적합합니다. 예를 들면 다음과 같습니다. - 회사 소개 페이지 - 서비스 소개 페이지 - 문서 페이지 - 블로그 글 상세 페이지 이미 만들어진 HTML을 제공하기 때문에 속도가 빠르고 서버 부하가 적습니다. 하지만 데이터가 자주 바뀌는 화면에는 적합하지 않을 수 있습니다. *** 3.3 ISR: Incremental Static Regeneration ISR은 정적 페이지의 장점과 데이터 갱신의 장점을 함께 가져가는 방식입니다. 정적 페이지를 제공하되, 일정 시간이 지나면 페이지를 다시 생성할 수 있습니다. 예를 들어 60초마다 데이터를 갱신하고 싶다면 다음처럼 사용할 수 있습니다. const res = await fetch("https://api.example.com/posts", { next: { revalidate: 60 }, }); 이 방식은 블로그, 상품 상세, 공지사항처럼 빠른 응답이 필요하지만 데이터가 가끔 바뀌는 페이지에 적합합니다. *** 4. Route Handlers로 API 만들기 Next.js에서는 "app/api" 경로 안에 "route.ts" 파일을 만들면 API를 구성할 수 있습니다. 예를 들어 "/api/health" API를 만들고 싶다면 다음처럼 작성합니다. // app/api/health/route.ts import { NextResponse } from "next/server"; export async function GET() { return NextResponse.json({ status: "ok", message: "서버가 정상 동작 중입니다.", }); } 이제 브라우저에서 "/api/health"로 접근하면 JSON 응답을 받을 수 있습니다. 간단한 백엔드 기능, 인증 체크, 외부 API 프록시, 관리자용 내부 API 등을 만들 때 유용합니다. 다만 규모가 큰 백엔드, 복잡한 배치 처리, 대규모 트랜잭션 처리가 필요한 시스템이라면 별도의 백엔드 서버를 분리하는 것이 더 적합할 수 있습니다. *** 5. Metadata API로 SEO 설정하기 Next.js에서는 "metadata" 객체나 "generateMetadata" 함수를 사용해 페이지의 SEO 정보를 설정할 수 있습니다. // app/blog/[id]/page.tsx import type { Metadata } from "next"; export const metadata: Metadata = { title: "Next.js 입문 가이드", description: "Next.js의 핵심 개념과 사용 이유를 정리한 글입니다.", openGraph: { title: "Next.js 입문 가이드", description: "React 기반 풀스택 프레임워크 Next.js 알아보기", type: "article", }, }; 페이지별 제목, 설명, OG 태그를 체계적으로 관리할 수 있기 때문에 블로그, 랜딩 페이지, 서비스 소개 페이지에서 특히 유용합니다. *** 6. Image Optimization 웹 서비스에서 이미지 최적화는 성능에 큰 영향을 줍니다. Next.js는 "next/image" 컴포넌트를 제공합니다. import Image from "next/image"; export default function Profile() { return ( <Image src="/profile.png" width={500} height={500} alt="프로필 이미지" /> ); } "next/image"를 사용하면 이미지 크기 최적화, lazy loading, 레이아웃 안정성 개선 등에 도움을 받을 수 있습니다. 일반 "<img>" 태그보다 설정은 조금 더 필요할 수 있지만, 실제 서비스에서는 성능과 사용자 경험 측면에서 장점이 큽니다. *** 7. 프로젝트 생성 방법 Next.js 프로젝트는 다음 명령어로 생성할 수 있습니다. npx create-next-app@latest my-next-app 실행하면 TypeScript 사용 여부, ESLint 사용 여부, Tailwind CSS 사용 여부, App Router 사용 여부 등을 선택할 수 있습니다. 프로젝트 생성 후 실행은 다음과 같습니다. cd my-next-app npm run dev 기본 개발 서버는 보통 다음 주소에서 확인할 수 있습니다. http://localhost:3000 *** 8. 기본 폴더 구조 예시 일반적인 App Router 기반 프로젝트 구조는 다음과 같이 구성할 수 있습니다. my-next-app ├─ app │ ├─ api │ │ └─ health │ │ └─ route.ts │ ├─ components │ │ └─ Counter.tsx │ ├─ posts │ │ ├─ page.tsx │ │ └─ [id] │ │ └─ page.tsx │ ├─ layout.tsx │ └─ page.tsx ├─ public │ └─ images ├─ styles ├─ next.config.ts ├─ package.json └─ tsconfig.json 프로젝트 규모가 커지면 "components", "lib", "hooks", "types", "services" 같은 폴더를 추가해서 역할별로 분리하는 것이 좋습니다. 예를 들어 다음과 같이 구성할 수 있습니다. src ├─ app ├─ components ├─ lib ├─ services ├─ hooks ├─ types └─ styles *** 9. Next.js가 적합한 경우 Next.js는 다음과 같은 프로젝트에 잘 어울립니다. - SEO가 중요한 서비스 - 블로그, 문서, 랜딩 페이지 - 관리자 페이지와 사용자 페이지가 함께 있는 웹 서비스 - React 기반으로 빠르게 서비스를 만들고 싶은 경우 - 서버 렌더링과 정적 생성을 함께 사용하고 싶은 경우 - 프론트엔드 중심이지만 간단한 API도 같이 필요한 경우 특히 페이지별로 렌더링 전략을 다르게 가져갈 수 있다는 점이 큰 장점입니다. 예를 들어 서비스 소개 페이지는 SSG로 만들고, 관리자 대시보드는 SSR로 만들고, 게시글 상세는 ISR로 구성할 수 있습니다. *** 10. Next.js 사용 시 주의할 점 Next.js가 모든 상황에서 정답은 아닙니다. 다음과 같은 점은 미리 고려하는 것이 좋습니다. 10.1 서버와 클라이언트 경계 이해가 필요합니다 App Router에서는 Server Component와 Client Component의 차이를 이해해야 합니다. 처음에는 왜 "useState"가 안 되는지, 왜 "window" 객체를 바로 사용할 수 없는지 헷갈릴 수 있습니다. 서버에서 실행되는 코드와 브라우저에서 실행되는 코드를 구분하는 습관이 필요합니다. *** 10.2 캐싱 정책을 신중하게 잡아야 합니다 Next.js는 성능을 위해 여러 캐싱 기능을 제공합니다. 하지만 어떤 데이터는 항상 최신이어야 하고, 어떤 데이터는 일정 시간 캐싱해도 됩니다. 서비스 특성에 맞게 캐싱 전략을 정하지 않으면 화면에 오래된 데이터가 보이거나, 반대로 서버 요청이 너무 많아질 수 있습니다. *** 10.3 백엔드 역할을 어디까지 맡길지 정해야 합니다 Next.js의 Route Handlers로 API를 만들 수 있지만, 모든 백엔드 기능을 Next.js 안에 넣는 것이 항상 좋은 것은 아닙니다. 대규모 서비스에서는 Spring Boot, NestJS, Django, FastAPI 같은 별도 백엔드와 Next.js 프론트엔드를 분리하는 구조도 많이 사용합니다. 예를 들어 화면은 Next.js로 구성하고, 업무 로직과 데이터 처리는 Spring Boot API 서버에서 처리하는 구조를 많이 사용합니다. 사용자 브라우저 ↓ Next.js 프론트엔드 ↓ Spring Boot API 서버 ↓ Database 이 구조를 사용하면 프론트엔드와 백엔드 역할을 명확히 분리할 수 있습니다. *** 11. 간단한 예제 페이지 다음은 Next.js에서 작성할 수 있는 간단한 메인 페이지 예시입니다. // app/page.tsx import Link from "next/link"; export default function HomePage() { return ( <main> <h1>Next.js 블로그</h1> <p> Next.js는 React 기반의 풀스택 웹 프레임워크입니다. </p> <ul> <li> <Link href="/posts">게시글 목록 보기</Link> </li> <li> <Link href="/about">서비스 소개</Link> </li> </ul> </main> ); } "Link" 컴포넌트를 사용하면 페이지 이동을 최적화할 수 있습니다. *** 12. 마무리 Next.js는 React를 기반으로 실제 서비스 개발에 필요한 기능을 통합해서 제공하는 프레임워크입니다. React만 사용할 때 직접 구성해야 했던 라우팅, 서버 렌더링, 정적 페이지 생성, 이미지 최적화, SEO 설정, API 구성 등을 Next.js에서는 비교적 체계적으로 처리할 수 있습니다. 처음에는 App Router, Server Component, Client Component, 캐싱 정책이 조금 어렵게 느껴질 수 있습니다. 하지만 이 개념들을 이해하면 단순한 프론트엔드 화면뿐만 아니라 SEO가 필요한 웹 서비스, 블로그, 관리자 페이지, 풀스택 서비스까지 더 효율적으로 만들 수 있습니다. React를 어느 정도 알고 있고 실제 서비스를 만들고 싶다면 Next.js는 충분히 배워볼 만한 프레임워크입니다. *** 참고 자료 - Next.js 공식 문서: https://nextjs.org/docs - Next.js App Router 문서: https://nextjs.org/docs/app - Next.js Route Handlers 문서: https://nextjs.org/docs/app/getting-started/route-handlers - Next.js Metadata 문서: https://nextjs.org/docs/app/getting-started/metadata-and-og-images - Next.js Image Optimization 문서: https://nextjs.org/docs/app/getting-started/images - Next.js 공식 블로그: https://nextjs.org/blog

June 30, 2026