앞에서 프로세스와 스레드가 무엇인지 알아봤다. 프로그램이 실행되면 프로세스가 되고, 여러 프로세스가 동시에 실행될 수 있다. 그렇다면 한 가지 문제가 생긴다. CPU는 한정되어 있는데, 실행해야 할 프로세스는 여러 개라면 CPU를 누구에게 먼저 줘야 할까? 운영체제는 이 문제를 해결하기 위해 CPU 스케줄링 을 사용한다. CPU 스케줄링은 여러 프로세스 중에서 CPU를 사용할 프로세스를 선택하고, CPU 사용 순서를 결정하는 것 이다. 1. 프로세스마다 우선순위가 다르다 모든 프로세스가 똑같은 중요도를 가지는 것은 아니다. 운영체제는 프로세스의 우선순위 를 고려하여 CPU를 할당한다. 예를 들어 어떤 프로세스는 CPU를 오래 사용해야 하고, 어떤 프로세스는 잠깐 CPU를 사용한 뒤 입출력을 기다려야 할 수도 있다. 프로세스의 특성을 이해하기 위해 크게 두 종류로 나눌 수 있다. CPU 바운드 프로세스 CPU를 많이 사용하는 프로세스다. 예를 들어 복잡한 계산처럼 CPU에서 처리해야 하는 작업이 많은 경우가 이에 해당한다. I/O 바운드 프로세스 입출력 작업을 많이 사용하는 프로세스다. CPU를 사용하다가도 파일 읽기나 입력 등의 입출력 작업을 기다리는 상태가 될 수 있다. 즉, CPU 바운드 → CPU를 사용하는 시간이 상대적으로 김 I/O 바운드 → CPU 사용 후 입출력을 기다리는 경우가 많음 운영체제는 이러한 프로세스들의 특성과 우선순위를 고려하면서 CPU를 배분한다. 2. 프로세스는 어디에서 기다릴까? 실행할 프로세스가 많다고 해서 모든 프로세스가 CPU를 바로 사용할 수 있는 것은 아니다. 프로세스들은 자신의 상태에 따라 여러 큐에서 기다리게 된다. 대표적으로 준비 큐(Ready Queue) 와 대기 큐(Waiting Queue) 가 있다. 준비 큐 CPU를 사용할 준비가 되어 있는 프로세스들이 기다리는 곳이다. 프로세스 A ─┐ 프로세스 B ─┼→ 준비 큐 → CPU 프로세스 C ─┘ CPU 스케줄러는 준비 큐에 있는 프로세스 중 하나를 선택해 CPU를 할당한다. 대기 큐 입출력 작업 등을 기다리는 프로세스가 머무르는 곳이다. CPU ↓ 입출력 요청 ↓ 대기 큐 ↓ 입출력 완료 ↓ 준비 큐 따라서 프로세스는 실행되는 동안 준비 큐와 대기 큐 등을 오가게 된다. 3. 선점형과 비선점형 CPU 스케줄링 방법을 이해할 때 중요한 기준 중 하나가 선점 여부 다. 비선점형 스케줄링 한 프로세스가 CPU를 사용하기 시작하면, 스스로 CPU를 반납할 때까지 다른 프로세스가 강제로 CPU를 빼앗을 수 없는 방식 이다. 프로세스 A ████████████ ↓ CPU 반납 ↓ 프로세스 B ████████ 프로세스가 실행을 끝내거나 대기 상태로 들어가야 다음 프로세스가 CPU를 사용할 수 있다. 선점형 스케줄링 현재 실행 중인 프로세스의 CPU를 다른 프로세스가 강제로 빼앗을 수 있는 방식 이다. 프로세스 A ██████ ↓ 선점 프로세스 B ███████ 예를 들어 더 높은 우선순위의 프로세스가 준비되었다면 현재 실행 중인 프로세스를 멈추고 새로운 프로세스에게 CPU를 할당할 수 있다. 둘의 차이를 정리하면 다음과 같다. 구분 선점형 비선점형 CPU 강제 회수 가능 불가능 프로세스 전환 필요에 따라 가능 CPU 반납 후 전환 응답성 상대적으로 높일 수 있음 상대적으로 낮을 수 있음 특징 여러 프로세스를 빠르게 번갈아 실행 가능 구조가 비교적 단순 4. CPU 스케줄링 알고리즘 CPU를 어떤 순서로 할당할지는 여러 가지 방법으로 결정할 수 있다. 대표적인 CPU 스케줄링 알고리즘에는 다음과 같은 것들이 있다. FCFS SJF Round Robin SRT Priority Scheduling Multilevel Queue Multilevel Feedback Queue 각 알고리즘마다 프로세스를 선택하는 기준이 다르다. 4-1. FCFS — First Come First Served 먼저 온 프로세스부터 처리하는 방식 이다. 말 그대로 먼저 준비 큐에 들어온 프로세스가 먼저 CPU를 사용한다. 준비 큐 A → B → C ↓ A → B → C 은행에서 줄을 선 순서대로 처리하는 것과 비슷하게 생각할 수 있다. 구조가 단순하다는 특징이 있지만, 먼저 실행된 프로세스의 작업이 너무 오래 걸리면 뒤의 프로세스들이 오랫동안 기다릴 수 있다. 4-2. SJF — Shortest Job First 실행 시간이 짧은 프로세스를 먼저 실행하는 방식 이다. A : ██████████ B : ███ C : █████ → B → C → A CPU를 짧게 사용하는 프로세스를 먼저 처리하면 전체적으로 프로세스가 기다리는 시간을 줄이는 데 도움이 될 수 있다. 4-3. Round Robin Round Robin은 일정한 시간만큼씩 CPU를 돌아가면서 사용하는 방식 이다. 이때 각 프로세스에게 할당되는 일정한 시간을 타임 슬라이스(Time Slice) 라고 한다. 예를 들어 타임 슬라이스가 2라면, A → B → C → A → B → C ... 각 프로세스가 CPU를 조금씩 나누어 사용한다. 한 프로세스가 CPU를 계속 독점하지 않도록 하기 때문에 여러 프로세스가 번갈아 실행되는 환경에서 사용할 수 있다. 4-4. SRT — Shortest Remaining Time SRT는 남은 실행 시간이 짧은 프로세스를 우선하는 방식 이다. SJF와 비슷하지만, 현재 실행 중인 프로세스보다 남은 실행 시간이 더 짧은 프로세스가 등장하면 CPU를 넘겨줄 수 있다. 노트에서는 SRT를 SJF와 Round Robin의 특징이 결합된 형태로 정리하고 있다. 4-5. Priority Scheduling 말 그대로 우선순위가 높은 프로세스부터 실행하는 방식 이다. 프로세스 A → 우선순위 3 프로세스 B → 우선순위 1 프로세스 C → 우선순위 2 ↓ B → C → A 우선순위의 기준은 시스템의 목적이나 프로세스의 특성에 따라 정해질 수 있다. 다만 우선순위가 낮은 프로세스가 계속해서 실행되지 못하는 문제가 발생할 수 있다. 5. 여러 개의 큐를 사용하는 방법 프로세스를 하나의 큐에서만 관리하는 것이 아니라 여러 개의 큐로 나누어 관리하는 방법 도 있다. Multilevel Queue 프로세스의 특성이나 우선순위 등에 따라 여러 개의 큐를 만들어 관리하는 방식이다. 예를 들어, 높은 우선순위 ┌──────────────┐ │ 큐 1 │ └──────────────┘ ┌──────────────┐ │ 큐 2 │ └──────────────┘ ┌──────────────┐ │ 큐 3 │ └──────────────┘ 낮은 우선순위 각 큐마다 서로 다른 스케줄링 방식을 사용할 수도 있다. Multilevel Feedback Queue Multilevel Feedback Queue는 여러 개의 큐를 사용하면서 프로세스의 실행 특성에 따라 큐를 이동시킬 수 있는 방식 이다. 프로세스가 CPU를 사용하는 방식에 따라 다른 큐로 이동할 수 있기 때문에 하나의 기준만 사용하는 것보다 유연하게 프로세스를 관리할 수 있다. 노트에서는 Multilevel Feedback Queue를 다양한 스케줄링 상황에 적용할 수 있는 일반적인 형태로 정리하고 있다. 6. CPU 스케줄링을 왜 알아야 할까? 지금까지 살펴본 내용을 하나로 연결해보자. 컴퓨터에서는 여러 프로세스가 동시에 실행되고 있다. 하지만 CPU는 한정된 자원이기 때문에 운영체제가 모든 프로세스에게 CPU를 무작정 나눠줄 수는 없다. 그래서 운영체제는 여러 프로세스 ↓ 준비 큐 ↓ CPU 스케줄러 ↓ 실행할 프로세스 선택 ↓ CPU 할당 과 같은 과정을 통해 CPU를 관리한다. 그리고 어떤 프로세스를 먼저 실행할 것인지 결정하는 방법에 따라 FCFS, SJF, Round Robin, SRT, Priority Scheduling, Multilevel Queue, Multilevel Feedback Queue 등의 알고리즘을 사용할 수 있다. 결국 CPU 스케줄링의 핵심은 한정된 CPU 자원을 여러 프로세스에게 어떻게 배분할 것인가 라고 볼 수 있다. 마무리 이번 글에서는 운영체제가 여러 프로세스에게 CPU를 할당하는 CPU 스케줄링 에 대해 알아봤다. 핵심 내용을 정리하면 다음과 같다. CPU 스케줄링 │ ├─ 프로세스의 우선순위 고려 │ ├─ 준비 큐 / 대기 큐 │ ├─ 선점형 / 비선점형 │ └─ 스케줄링 알고리즘 ├─ FCFS ├─ SJF ├─ Round Robin ├─ SRT ├─ Priority ├─ Multilevel Queue └─ Multilevel Feedback Queue 그런데 여기서 또 하나의 문제가 생긴다. 여러 프로세스가 같은 자원을 사용한다면 어떻게 해야 할까? 여러 프로세스가 동시에 하나의 자원에 접근하면 실행 순서에 따라 결과가 달라지는 문제가 발생할 수 있다. 다음 글에서는 이러한 문제를 해결하기 위한 프로세스 동기화 와 함께 임계 구역, 경쟁 조건, 뮤텍스, 세마포어, 모니터 를 살펴본다.
지난 글에서는 운영체제가 무엇인지, 그리고 운영체제가 CPU·메모리·입출력장치 등의 자원을 관리한다는 것을 알아봤다. 그렇다면 한 가지 궁금증이 생긴다. 운영체제는 실제로 실행 중인 프로그램을 어떻게 관리할까? 이번 글에서는 운영체제가 프로그램을 관리하기 위해 사용하는 핵심 개념인 프로세스와 스레드 를 알아본다. 프로세스의 구조부터 PCB, 문맥 교환, 프로세스 상태, 그리고 멀티프로세스와 멀티스레드까지 순서대로 정리해보자. 1. 프로세스란? 프로그램과 프로세스 먼저 프로그램과 프로세스를 구분해야 한다. 프로그램(Program) 은 저장장치에 존재하는 실행할 수 있는 파일이다. 그리고 이 프로그램을 메모리에 적재하고 실행하면 프로세스(Process) 가 된다. 즉, 프로세스 = 실행 중인 프로그램 이라고 정리할 수 있다. 예를 들어 컴퓨터에 게임 프로그램이 설치되어 있다고 생각해보자. 게임을 실행하지 않은 상태에서는 단순히 저장장치에 있는 프로그램이지만, 실행하는 순간 운영체제가 해당 프로그램을 메모리에 적재하고 실행을 관리한다. 이때 실행 중인 프로그램을 프로세스라고 한다. 2. 포그라운드 프로세스와 백그라운드 프로세스 프로세스는 사용자가 볼 수 있는 공간에서 실행되는지에 따라 크게 나누어 볼 수 있다. 포그라운드 프로세스 사용자가 볼 수 있는 공간에서 실행되는 프로세스 다. 예를 들어 우리가 직접 실행해서 사용하는 프로그램이 이에 해당한다. 백그라운드 프로세스 사용자가 직접 보고 있지 않은 공간에서 실행되는 프로세스 다. 사용자와 상호작용하지 않고 정해진 작업을 수행하는 프로세스를 데몬(daemon) 또는 서비스(service) 라고 부르기도 한다. 즉, 화면에 보이지 않는다고 해서 프로그램이 실행되고 있지 않은 것은 아니다. 3. 프로세스 제어 블록(PCB) 모든 프로세스는 실행을 위해 CPU가 필요하다. 하지만 CPU 자원은 한정되어 있다. 따라서 여러 프로세스가 CPU를 사용하려면 운영체제가 프로세스들을 관리하면서 CPU를 번갈아 할당해야 한다. 이때 운영체제가 프로세스를 관리하기 위해 사용하는 자료구조가 바로 프로세스 제어 블록(PCB, Process Control Block) 이다. PCB란? PCB는 프로세스와 관련된 정보를 저장하는 자료구조 다. 프로세스 하나하나에 붙어 있는 태그 라고 생각하면 이해하기 쉽다. 프로세스가 생성되면 커널 영역에 PCB가 생성되고, 프로세스가 종료되면 PCB도 폐기된다. PCB에는 어떤 정보가 들어 있을까? 대표적으로 다음과 같은 정보가 저장된다. 정보 설명 프로세스 ID(PID) 프로세스를 식별하기 위한 고유 번호 레지스터 값 프로그램 카운터(PC) 및 각종 레지스터 값 프로세스 상태 실행 중인지, 대기 중인지 등의 상태 CPU 스케줄링 정보 언제, 어떤 순서로 CPU를 사용할지에 대한 정보 메모리 정보 프로세스가 저장된 주소와 페이지 테이블 정보 파일·입출력장치 정보 할당된 입출력장치 및 열린 파일 목록 특히 PID(Process ID) 는 프로세스를 식별하기 위한 번호다. 학교에서 학생을 학번으로 구분하거나 회사에서 사번으로 구분하는 것과 비슷하게 생각할 수 있다. 4. 문맥 교환 CPU는 한 번에 하나의 프로세스만 실행할 수 있다. 그렇다면 여러 프로세스가 동시에 실행되는 것처럼 보이는 이유는 무엇일까? 프로세스들이 CPU를 아주 빠르게 번갈아 사용하기 때문이다. 예를 들어 다음과 같은 상황을 생각해보자. 프로세스 A 실행 ↓ 프로세스 A의 상태 저장 ↓ 프로세스 B의 상태 복구 ↓ 프로세스 B 실행 ↓ 프로세스 B의 상태 저장 ↓ 프로세스 A의 상태 복구 ↓ 프로세스 A 실행 이처럼 CPU가 한 프로세스에서 다른 프로세스로 실행을 넘길 때 기존 프로세스의 상태를 저장하고 새로운 프로세스의 상태를 복구하는 과정을 문맥 교환(Context Switch) 이라고 한다. 문맥(Context)이란? 하나의 프로세스가 실행을 이어가기 위해 기억해야 하는 정보를 말한다. 운영체제는 이러한 정보를 PCB에 저장해두었다가 다시 해당 프로세스를 실행할 때 복구한다. 즉, 문맥 교환 = 기존 프로세스의 문맥을 저장하고 새로운 프로세스의 문맥을 불러오는 과정 이라고 이해하면 된다. 5. 프로세스의 메모리 영역 프로세스가 실행되면 메모리에는 여러 영역이 구성된다. 대표적으로 다음 네 가지 영역으로 나눌 수 있다. 영역 특징 저장되는 것 코드 영역 정적 할당, 읽기 전용 실행할 기계어 명령어 데이터 영역 정적 할당 전역 변수 등 힙 영역 동적 할당, 크기 변경 가능 프로그래머가 동적으로 할당하는 데이터 스택 영역 동적 할당, 크기 변경 가능 지역 변수, 매개변수 등 코드 영역 프로그램을 실행하기 위한 기계어 명령어 가 저장되는 영역이다. 읽기 전용으로 관리된다. 데이터 영역 프로그램이 실행되는 동안 유지되어야 하는 데이터가 저장된다. 대표적으로 전역 변수처럼 프로그램이 종료될 때까지 유지되는 데이터가 해당한다. 힙 영역 프로그래머가 직접 메모리를 할당할 수 있는 영역이다. 사용한 메모리를 제대로 반환하지 않으면 메모리 누수 가 발생할 수 있다. 스택 영역 지역 변수나 매개변수처럼 함수 실행 과정에서 잠시 사용되는 데이터가 저장된다. 힙과 스택은 어느 방향으로 자랄까? 프로세스의 메모리 구조에서는 힙과 스택이 서로 반대 방향으로 확장된다. 높은 주소 ┌─────────────┐ │ 스택 │ │ ↓ │ ├─────────────┤ │ │ │ 여유 공간 │ │ │ ├─────────────┤ │ ↑ │ │ 힙 │ ├─────────────┤ │ 데이터 │ ├─────────────┤ │ 코드 │ └─────────────┘ 낮은 주소 힙은 낮은 주소에서 높은 주소 방향으로, 스택은 높은 주소에서 낮은 주소 방향으로 쌓인다. 이렇게 서로 반대 방향으로 확장되도록 구성하면 두 영역이 효율적으로 메모리 공간을 사용할 수 있다. 6. 프로세스의 상태 프로세스는 실행되는 동안 항상 같은 상태에 머물러 있는 것이 아니다. 필요에 따라 여러 상태를 오가게 된다. 대표적인 프로세스 상태는 다음과 같다. 생성 ↓ 준비 ─────→ 실행 ─────→ 종료 ↑ │ │ │ │ ↓ └───────── 대기 생성 상태 프로세스가 이제 막 생성되어 메모리에 적재되고 PCB를 할당받은 상태다. 준비 상태 CPU를 할당받으면 바로 실행할 수 있지만, 아직 자신의 차례가 오지 않아 기다리는 상태다. 준비 상태에서 실행 상태로 넘어가는 것을 디스패치(Dispatch) 라고 한다. 실행 상태 CPU를 할당받아 실제로 실행 중인 상태다. 실행 중인 프로세스는 다음과 같은 이유로 다른 상태로 이동할 수 있다. 할당된 시간을 모두 사용함 → 준비 상태 입출력 작업이 필요함 → 대기 상태 대기 상태 입출력 작업처럼 CPU가 아닌 다른 작업이 완료되기를 기다리는 상태다. 입출력이 완료되면 다시 준비 상태로 돌아간다. 종료 상태 프로세스의 실행이 끝난 상태다. 운영체제는 프로세스의 PCB를 폐기하고 할당된 메모리를 정리한다. 7. 프로세스 계층 구조 운영체제는 프로세스를 단순히 나열해서 관리하는 것이 아니라 부모와 자식 관계 로 묶어 관리하기도 한다. 부모 프로세스 새로운 프로세스를 생성한 프로세스 자식 프로세스 부모 프로세스에 의해 생성된 프로세스 부모 프로세스 │ ├── 자식 프로세스 A │ ├── 자식 프로세스 B │ └── 자식 프로세스 C 부모 프로세스와 자식 프로세스는 서로 다른 프로세스이기 때문에 각각 다른 PID를 가진다. 일부 운영체제에서는 자식 프로세스가 자신의 부모 프로세스 PID인 PPID 를 확인할 수도 있다. 다만 운영체제마다 프로세스를 관리하는 방식에는 차이가 있으며, 원본 정리에서는 Windows는 프로세스를 계층적으로 관리하지 않는다는 차이점 을 언급하고 있다. 8. 프로세스 생성 — fork와 exec 부모 프로세스가 새로운 자식 프로세스를 만들고, 자식 프로세스가 부모와 다른 프로그램을 실행하게 만드는 과정에는 fork와 exec 시스템 호출 이 사용된다. fork fork 는 자신의 복사본을 자식 프로세스로 생성하는 시스템 호출 이다. 이 과정에서 메모리 내용이나 열린 파일 목록 등의 정보가 자식 프로세스에 상속될 수 있다. 쉽게 표현하면, fork = 부모 프로세스를 복제해서 자식 프로세스를 만든다. exec exec 는 자신의 메모리 공간을 다른 프로그램으로 교체하는 시스템 호출 이다. 이를 이용하면 자식 프로세스가 부모 프로세스와 다른 새로운 프로그램을 실행할 수 있다. 정리하면, 부모 프로세스 │ fork ↓ 자식 프로세스 │ exec ↓ 다른 프로그램 실행 즉, fork 는 복제 , exec 는 프로그램 교체 라고 기억하면 쉽다. 9. 스레드란? 이제 프로세스보다 한 단계 더 작은 실행 단위를 살펴보자. 스레드(Thread) 는 프로세스를 구성하는 실행 흐름의 단위 다. 하나의 프로세스는 하나 이상의 스레드를 가질 수 있다. 따라서 하나의 프로세스 안에서 여러 실행 흐름이 동시에 동작하도록 만들 수 있다. 예를 들어 어떤 프로그램이 다음과 같은 작업을 수행한다고 생각해보자. 프로세스 ├─ 스레드 1 → 사용자 입력 처리 ├─ 스레드 2 → 파일 처리 └─ 스레드 3 → 화면 업데이트 하나의 프로세스 안에서도 여러 작업을 나누어 처리할 수 있는 것이다. 10. 스레드는 무엇을 공유할까? 스레드는 프로세스에 속해 있기 때문에 프로세스의 자원을 공유할 수 있다. 하지만 모든 정보를 공유하는 것은 아니다. 스레드마다 독립적으로 가지는 것 스레드 ID 프로그램 카운터(PC) 레지스터 값 스택 각 스레드는 자신의 실행 흐름을 관리해야 하기 때문에 이러한 정보는 독립적으로 가지고 있어야 한다. 같은 프로세스의 스레드가 공유하는 것 코드 영역 데이터 영역 힙 영역 프로세스가 연 파일 등의 시스템 자원 즉, 스레드는 실행에 필요한 최소한의 정보는 각자 가지고, 프로세스의 자원은 공유한다. 이 구조가 멀티스레드의 중요한 특징이다. 11. 멀티프로세스와 멀티스레드 여기까지 이해했다면 두 개념을 비교해볼 수 있다. 멀티프로세스 여러 프로세스를 동시에 실행하는 방식이다. 각 프로세스는 기본적으로 독립적인 자원을 가진다. 멀티스레드 하나의 프로세스 안에서 여러 스레드를 동시에 실행하는 방식이다. 같은 프로세스에 속한 스레드들은 프로세스의 자원을 공유한다. 구분 멀티프로세스 멀티스레드 자원 공유 독립적 프로세스의 자원 공유 통신 및 협력 상대적으로 어렵고 비용이 큼 자원 공유로 상대적으로 유리 안정성 한 프로세스의 오류가 다른 프로세스에 미치는 영향이 상대적으로 적음 한 스레드의 오류가 전체 프로세스에 영향을 줄 수 있음 메모리 효율 동일 작업에서 중복 적재가 발생할 수 있음 필요한 정보만 별도로 유지하므로 상대적으로 효율적 둘 중 하나가 항상 더 좋은 것은 아니다. 각각의 구조가 가진 특징과 장단점이 있기 때문에 프로그램의 목적과 상황에 따라 적절한 방식을 선택해야 한다. 마무리 이번 글에서는 프로세스와 스레드가 무엇인지 , 그리고 운영체제가 실행 중인 프로그램을 어떻게 관리하는지 살펴봤다. 전체 흐름을 정리하면 다음과 같다. 프로그램 ↓ 실행 프로세스 ↓ PCB를 통해 운영체제가 관리 ↓ CPU를 할당받아 실행 ↓ 프로세스 상태가 계속 변화 ↓ 하나의 프로세스 안에서 여러 실행 흐름을 만들 수 있음 ↓ 스레드 특히 기억해야 할 개념을 정리하면 다음과 같다. 프로세스 → 실행 중인 프로그램 PCB → 프로세스의 정보를 저장하는 자료구조 문맥 교환 → 프로세스가 바뀔 때 상태를 저장하고 복구하는 과정 프로세스 상태 → 생성, 준비, 실행, 대기, 종료 fork → 프로세스 복제 exec → 실행할 프로그램 교체 스레드 → 프로세스를 구성하는 실행 흐름의 단위 멀티프로세스 → 여러 프로세스를 실행 멀티스레드 → 하나의 프로세스에서 여러 스레드를 실행 다음 글에서는 여러 프로세스가 CPU를 사용하려고 할 때 운영체제가 어떤 기준으로 CPU를 나누어 주는지 , 즉 CPU 스케줄링 에 대해 알아본다.
코드잇 데이터분석가 부트캠프를 통해 배우고 느낀 것을 적습니다. 강의/미션/위클리페이퍼/프로젝트로 나누어 작성합니다. 📅 2026.05.20 | 🗺️ 과정: 1개월차 ▓░░░░░░ | 📖 커리큘럼: EDA 숫자로 적혀 있다고 모두 숫자가 아니라는 것을 배우며, 데이터를 계산하기 전에 의미부터 읽는 습관을 만든 하루였다. 🗓️ 오늘의 일정 교시 시간 내용 1~3교시 09:00~12:00 EDA 강의, 컬럼의 역할 나누기 4~7교시 13:00~17:00 EDA 강의, 이상치와 상관관계, 그래프 해석 8~9교시 17:00~19:00 자습 오늘은 EDA 강의를 듣고, 내일은 같은 흐름을 직접 데이터에 적용하는 미션 4를 진행한다. 1️⃣ 컬럼부터 다시 보기 1-1. dtype은 저장 방식일 뿐이다 ❓ 의문. 컬럼이 float로 저장되어 있는데 왜 연속형이 아니라고 하는지 궁금했다. 📖 배움. dtype은 값이 컴퓨터에 저장된 방식이고, 변수의 종류는 값이 가진 의미로 정한다. 흡연 여부나 청력 같은 범주형 코드가 float로 보이는 이유는 대개 결측치가 섞여 있어서 정수 대신 실수로 저장되었기 때문이다. 즉 의미는 범주인데 저장만 실수인 상태이다. 그래서 info() 를 본 다음에는 컬럼을 하나씩 보며 이 숫자가 계산해도 되는 숫자인지를 다시 물어야 한다. 💭 지금까지는 숫자형이면 평균을 내도 된다고 당연하게 생각했다. 지역 코드의 평균이나 성별 코드의 평균은 계산은 되지만 아무 뜻이 없다는 말을 듣고, 계산이 되는 것과 의미가 있는 것은 다르다는 것을 처음 실감했다. 1-2. 컬럼의 역할을 다섯 가지로 나눈다 📖 배움. 컬럼을 식별자, 명목형, 순서형, 연속형, 특수 처리 변수로 나누면 각 컬럼에 무엇을 해야 하는지가 정해진다. 식별자는 행을 구분하는 용도라서 분석에서 빼고, 명목형은 이름표로 바꿔서 빈도를 본다. 순서형은 순서를 유지한 채 이름표로 바꾸고, 연속형은 평균과 분포를 본다. 특수 처리 변수는 숫자처럼 보이지만 규칙이 숨어 있는 컬럼이다. 아래는 이 기준을 작은 설문 표에 적용해 본 예시이다. import pandas as pd survey = pd.DataFrame({ '응답번호': [101, 102, 103, 104], # 식별자 '지역코드': [11, 26, 11, 41], # 명목형, 크기에 의미가 없다 '만족도': [3, 5, 4, 2], # 순서형, 순서에는 의미가 있다 '키': [160, 172, 165, 158], # 연속형 }) # 역할별로 컬럼을 묶어 두면 이후 처리가 쉬워진다 id_cols = ['응답번호'] nominal_cols = ['지역코드'] ordinal_cols = ['만족도'] numeric_cols = ['키'] print(survey[numeric_cols].mean()) # 평균은 연속형에만 낸다 💡 역할별로 변수 목록을 먼저 만들어 두면, 나중에 상관계수나 평균을 낼 때 어떤 컬럼을 넣어야 하는지 고민이 줄어들 것 같다. 분석을 시작하기 전에 해야 할 일이 하나 생긴 셈이다. 1-3. 특수한 코드는 결측으로 바꾸고, 표시 열을 남긴다 ❓ 의문. 측정값 열에 측정이 아닌 코드가 섞여 있으면 그 행을 버려야 하는지 궁금했다. 📖 배움. 버리지 않고 두 가지로 나눈다. 먼저 그 코드가 있었다는 사실을 별도의 표시 열로 남기고, 원래 열의 특수 코드는 결측으로 바꾼다. 그러면 원래 열은 순수한 측정값만 남아서 평균과 분포를 믿을 수 있고, 표시 열로는 그 상태에 해당하는 사람이 몇 명인지를 따로 볼 수 있다. 모든 알 수 없는 값을 결측 하나로 통일하는 효과도 있어서 이후 결측 처리가 한 방식으로 정리된다. import numpy as np # 나이가 모를 때 999로 적어 둔 표라고 가정한다 people = pd.DataFrame({'나이': [31, 999, 45, 28]}) people['나이_미응답'] = np.where(people['나이'] == 999, 1, 0) # 표시 열을 먼저 남긴다 people['나이'] = people['나이'].replace(999, np.nan) # 원래 열은 결측으로 바꾼다 print(people) 💭 값을 지우는 것이 아니라 정보를 나눠서 보관한다는 발상이 신선했다. 버리지 않고 두 개의 열로 살린다는 점이 마음에 들었다. 1-4. 결측치도 역할별로 다르게 채운다 📖 배움. 수치형 측정값은 중앙값으로, 범주형 코드는 최빈값으로 채우는 것이 기본이다. 중앙값을 쓰는 이유는 극단적인 값이 있어도 평균처럼 끌려가지 않기 때문이다. 다만 채우기 전에 결측이 왜 생겼는지, 채운 값이 분석을 왜곡하지 않는지를 먼저 생각해야 한다. 💭 7일차에 결측을 채우는 방법을 배웠을 때는 방법만 외웠는데, 오늘은 어떤 컬럼에 어떤 방법을 쓰는지를 기준으로 고르는 연습이 되었다. 2️⃣ 이상치를 어떻게 볼 것인가 2-1. describe 한 번으로 읽을 수 있는 것 📖 배움. 기술통계표는 값을 훑는 표가 아니라 질문을 던지는 표이다. 평균과 중앙값이 비슷하면 분포가 비교적 대칭이라는 단서가 되고, 평균이 중앙값보다 훨씬 크면 큰 값 몇 개가 평균을 끌어올렸을 가능성을 의심한다. 최솟값과 최댓값은 상식과 맞는지 확인하는 용도이다. 사람의 허리둘레가 비현실적으로 작거나 크다면 통계 이전에 입력 오류를 먼저 의심한다. 사분위수는 전체의 가운데가 어디에 몰려 있는지를 보여 준다. 💭 표 한 장에서 읽어 낼 수 있는 것이 이렇게 많다는 것이 놀라웠다. 앞으로 describe를 출력하면 숫자를 훑지 말고 하나씩 질문하면서 읽어야겠다고 생각했다. 2-2. IQR이 표준편차보다 믿을 만한 이유 ❓ 의문. 퍼짐 정도는 표준편차로도 알 수 있는데 왜 굳이 IQR을 따로 보는지 궁금했다. 📖 배움. 표준편차는 평균을 기준으로 계산하기 때문에 극단적인 값 하나에도 크게 부풀려진다. IQR은 가운데 절반이 퍼진 폭만 보므로 양쪽 끝의 이상한 값에 흔들리지 않는다. 이런 성질을 강건하다고 부른다. 이상치를 찾는 기준으로 쓰는 것도 이 때문이다. 아래는 직접 만든 예시로 경계를 계산해 본 것이다. scores = pd.Series([62, 70, 71, 74, 75, 78, 80, 150]) # 150은 입력 실수라고 가정 q1 = scores.quantile(0.25) q3 = scores.quantile(0.75) iqr = q3 - q1 lower = q1 - 1.5 * iqr # 아래쪽 경계 upper = q3 + 1.5 * iqr # 위쪽 경계 print(scores[(scores < lower) | (scores > upper)]) # 경계 밖의 값만 확인 💭 평균은 값 하나에 흔들리고 중앙값과 IQR은 흔들리지 않는다는 비교가 와닿았다. 평균만 보고 판단하는 것이 얼마나 위험한지 알게 되었다. 2-3. 지우기 전에 진단하고 처방한다 📖 배움. 이상치 처리는 식별, 진단, 처방, 검증의 네 단계로 한다. 식별은 상식과 시각화, 통계 기준으로 후보를 찾는 단계이다. 진단은 그 값이 명백한 오류인지 현실에서 충분히 나올 수 있는 극단값인지를 가르는 단계로, 여기서 처리 방향이 갈린다. 오류라면 삭제하거나 대체한다. 극단값이라면 오히려 중요한 신호일 수 있으므로 변환, 구간화, 상한과 하한으로 누르기, 별도 분석 중에서 고른다. 마지막으로 처리 전후에 결과가 어떻게 달라졌는지, 중요한 정보를 잃지 않았는지 확인한다. 💡 IQR 경계 밖에 있다는 것만으로 지우면 안 된다는 점이 핵심이다. 혈당이 아주 높은 사람은 오류가 아니라 분석해야 할 집단일 수 있다. 이상치는 먼저 이유를 묻는 대상이라고 정리했다. 3️⃣ 상관관계와 그래프 읽기 3-1. 상관계수는 방향과 강도, 그리고 인과가 아니다 ❓ 의문. 상관계수가 높으면 한 변수가 다른 변수의 원인이라고 말해도 되는지 궁금했다. 📖 배움. 상관계수는 두 변수가 얼마나 일관되게 함께 움직이는지를 방향과 강도로 보여 줄 뿐이다. 양의 상관이면 같이 오르고, 음의 상관이면 한쪽이 오를 때 다른 쪽이 내려간다. 절댓값이 클수록 관계가 강하다. 하지만 원인과 결과를 말해 주지는 않는다. 눈에 보이지 않는 제3의 요인이 둘을 함께 움직이는 경우가 많기 때문이다. 비 오는 날 우산 판매량과 지각 건수가 함께 늘어도, 우산이 지각의 원인은 아니다. 💭 상관관계가 높다는 말을 들으면 원인이라고 읽는 습관이 있었다. 앞으로 "관련이 있다" 정도로만 쓰는 연습을 해야겠다고 느꼈다. 3-2. 산점도는 방향, 형태, 강도 순서로 읽는다 📖 배움. 산점도는 두 수치형 변수의 관계를 확인하는 가장 첫 번째 도구이다. 점들이 어느 쪽으로 뻗는지가 방향이고, 직선인지 곡선인지가 형태이고, 점들이 선 주위에 얼마나 모여 있는지가 강도이다. 점이 촘촘할수록 한 변수 값으로 다른 변수 값을 비교적 잘 짐작할 수 있다는 뜻이다. 구름처럼 흩어져 있다면 관계가 약한 것이다. 3-3. 페어플롯과 히트맵은 역할이 다르다 📖 배움. 페어플롯은 여러 변수 쌍의 산점도와 각 변수의 분포를 한 번에 보여 주므로 모양과 이상한 점을 탐색하는 용도이다. 히트맵은 상관계수를 숫자와 색으로 요약하므로 어떤 관계가 가장 센지를 비교하는 용도이다. 변수가 많아지면 페어플롯은 읽기 어려워져서 몇 개로 줄여서 쓴다. 또 둘 다 직선 관계 위주로 보기 때문에 곡선 관계는 놓칠 수 있다. import seaborn as sns import matplotlib.pyplot as plt # 변수가 많으면 페어플롯이 무거우니 일부만 뽑아서 그린다 small = survey[['키']].copy() small['몸무게'] = [52, 68, 60, 49] sns.heatmap(small.corr(), annot=True, cmap='Blues') # 숫자와 색으로 요약한다 plt.title('상관계수 히트맵 예시') plt.show() 💭 그래프를 하나만 쓰는 것이 아니라 하나는 모양을, 하나는 수치를 맡는다는 구분이 머릿속에 정리되었다. 둘을 함께 보면 서로의 약점을 채워 준다. 3-4. 지표 하나로 판단하면 놓치는 것이 있다 📖 배움. 연령대별로 여러 건강 지표를 막대그래프로 나란히 놓으면 지표마다 움직임이 다르다. 어떤 지표는 나이가 들수록 내려가고, 어떤 지표는 오르거나 높은 채로 유지된다. 한 지표만 보고 판단하면 다른 지표에서 드러나는 위험을 놓칠 수 있다는 점이 이 비교의 핵심이었다. 그래서 의사결정 기준도 하나가 아니라 여러 지표를 조합해서 세워야 한다. 💡 그래프를 읽는다는 것은 추세를 말하는 데서 끝나지 않고 "그래서 어떤 기준을 바꿔야 하는가"까지 이어져야 한다는 것을 알았다. 해석의 끝에는 결정이 있어야 한다. 3-5. 막대그래프가 못 보여 주는 것은 바이올린 플롯으로 📖 배움. 막대그래프는 평균 하나만 보여 준다. 바이올린 플롯은 같은 그룹 안에서 값이 어느 구간에 몰려 있는지, 퍼짐이 어떤지까지 보여 준다. 평균이 비슷해도 분포 모양이 다를 수 있기 때문에 평균 비교 뒤에 보조로 쓰기 좋다. 📝 오늘의 정리 주제 내가 이해한 핵심 더 공부할 점 컬럼의 역할 dtype이 아니라 의미로 변수 종류를 정한다 역할별 컬럼 목록 만들기 특수 코드 표시 열을 남기고 원래 열은 결측으로 바꾼다 코드가 여러 종류일 때 정리 결측 처리 수치형은 중앙값, 범주형은 최빈값 채우기 전에 결측 원인 보기 이상치 지우기 전에 오류인지 극단값인지 진단한다 처리 전후 비교 검증 상관관계 방향과 강도를 보되 인과로 읽지 않는다 곡선 관계 확인 방법 그래프 해석 산점도는 방향, 형태, 강도 순서로 읽는다 해석을 결정으로 연결하기 📌 다음 일차 다음 일차: 13일차 (2026-05-21) 커리큘럼: 미션 4 - 다음 강의자료 예고: 오늘 배운 EDA 흐름을 큰 데이터에 직접 적용하고, 해설과 내 코드를 비교한다.
프론트엔드 개발에서는 동일한 크롬(Chrome)이나 사파리(Safari) 브라우저를 사용하더라도 실제 앱이 동작하는 운영체제(OS)에 따라 레이아웃 깨짐, 폰트 굵기 차이, 키보드 단축키 충돌 등 다양한 이슈가 발생한다. 스크롤바 처리 및 레이아웃 시프트 (Layout Shift) OS에 따라 스크롤바가 차지하는 공간의 방식이 완전히 다르다. macOS / iOS: 기본적으로 Overlay Scrollbar 형태이다. 스크롤할 때만 콘텐츠 위에 스크롤바가 떠오르고, 평소에는 레이아웃 공간을 차지하지 않는다 ( 100vw 와 100% 너비가 동일) Windows: 기본적으로 Classic Scrollbar 형태로, 스크롤바가 화면 우측의 일정 영역(약 15px~17px)을 고정 점유 문제 상황 모달이나 레이어 팝업이 열릴 때 overflow: hidden 을 부여하면, Windows에서는 스크롤바가 사라지면서 페이지 전체가 오른쪽으로 15px가량 덜컥거리는 레이아웃 시프트가 발생 마크업 제작 시 100vw 를 기준으로 지정한 가로 길이가 Windows에서는 스크롤바 너비를 포함해 계산되어 하단 스크롤이 원치 않게 발생할 수 있음 해결책 /* 현대 브라우저 및 현대 CSS 솔루션 */ html { scrollbar-gutter: stable; /* 스크롤바 유무와 상관없이 미리 공간을 예약 */ } 폰트 렌더링 및 안티앨리어싱 (Subpixel vs Grayscale) OS별 라스터라이징 엔진 차이로 인해 동일한 font-weight 를 적용하더라도 글자 두께나 선명도가 달라 보인다. macOS: 폰트 고유의 디자인과 굵기를 충실히 재현하도록 진하게 안티앨리어싱을 적용 Windows (ClearType): 서브픽셀 렌더링 기반으로 픽셀 그리드에 가독성을 최적화하여 상대적으로 얇고 가늘게 보임 크로스 OS 시스템 폰트 스택 (System Font Stack) OS마다 기본 탑재 폰트가 다르므로 폰트 패밀리 지정 시 꼼꼼한 fallback 설정이 필수적이다. body { font-family: -apple-system, BlinkMacSystemFont, /* macOS / iOS Apple 시스템 폰트 */ "Segoe UI", /* Windows 대표 시스템 폰트 */ Roboto, /* Android */ "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji"; /* 이모지 폰트 */ /* macOS에서 폰트가 지나치게 두꺼워 보이는 것 방지 */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } 키보드 이벤트 및 단축키 (Command vs Control) 웹 앱에서 커스텀 단축키(예: Cmd+K 검색창, Cmd+S 저장)를 구현할 때 OS별 키 매핑 차이가 존재한다. 입력 키 macOS Windows / Linux 메인 수식어 키 (Modifier) Meta (Cmd ⌘) Control (Ctrl) Delete / Backspace Backspace (Delete) / Fn+Delete Backspace / Delete 해결 방안 (Event Listener) const handleKeyDown = (e: KeyboardEvent) => { // OS 판별 조건문 const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; const isCmdOrCtrl = isMac ? e.metaKey : e.ctrlKey; if (isCmdOrCtrl && e.key === 'k') { e.preventDefault(); openSearchModal(); } }; 모바일 OS 및 웹킷(WebKit) 뷰포트 이슈 (iOS vs Android) 데스크톱 OS 외에도 모바일 OS 간 차이는 프론트엔드 UI를 파괴하는 가장 흔한 원인이 된다. 100vh 이슈 (iOS Safari) iOS Safari는 주소창과 하단 툴바의 고무줄 효과로 인해 100vh 가 화면의 실제 가시 영역보다 더 길게 계산되어 하단 요소가 잘리는 현상이 일어난다. 해결책: Modern CSS 단위인 100dvh (Dynamic Viewport Height) 또는 100svh (Small Viewport Height) 사용 .full-screen-modal { height: 100vh; /* Fallback */ height: 100dvh; /* 동적 뷰포트 대응 */ } Form Input 줌 현상 iOS Safari는 input 요소의 font-size 가 16px 미만일 때 포커스 시 자동으로 화면을 확대시킨다. 해결책: 모바일 프론트엔드 환경에서는 입력 폼의 font-size 를 최소 16px 이상으로 지정 입력 장치 및 제스처 동작 (Touch, Trackpad, Wheel) macOS (트랙패드): 좌우 스와이프 시 "뒤로가기/앞으로가기" 브라우저 제스처가 기본 작동한다. 가로 스크롤 영역( overflow-x: auto )이 있는 캐러셀/차트를 조작할 때 브라우저 뒤로가기 제스처와 충돌 가능 Windows (마우스 휠): 마우스 휠 단위 스크롤 속도가 트랙패드에 비해 뚝뚝 끊기는 디스크리트(Discrete) 스크롤 방식으로 동작하므로, Smooth Scroll 연출 시 보정이 필요 브라우저 뒤로가기 제스처 충돌 방지 .horizontal-scroll-container { overscroll-behavior-x: contain; /* 브라우저의 기본 페이지 전환 제스처 차단 */ } 이모지(Emoji) 및 파일 경로 렌더링 이모지 렌더링: Apple Color Emoji (macOS/iOS)와 Segoe UI Emoji (Windows)는 디자인 트렌드와 렌더링 방식이 완전히 달라, 서비스 아이콘 대용으로 이모지를 사용하면 OS별 분위기가 크게 달라진다. (가능하면 SVG 아이콘 시스템 권장) 파일명/경로 대소문자 구문: Windows: 파일 경로의 대소문자를 구분하지 않음 ( Header.tsx 와 header.tsx 를 동일하게 인식) macOS / Linux: 기본적으로 대소문자를 엄격히 구분 주의사항: Windows에서 개발 시 import Header from './header' 로 작성하더라도 local 실행은 잘 되지만, Linux 기반 CI/CD 빌드서버(GitHub Actions, Verce, AWS 등)로 전달될 때 빌드 에러가 발생
딥러닝 스터디 4차 세션 과제 | 위키독스 「09-01 토큰화(Tokenization)」 들어가며 4.1~4.2에서는 RNN과 LSTM에 torch.randn(1, 10, 5) 를 넣었다. "단어 10개, 단어마다 5차원 벡터"라고 가정한 가짜 입력 이었다. 이번 편부터는 진짜 문장을 다룬다. 문장이 모델에 들어가기까지는 세 단계를 거친다. "아 더빙.. 진짜 짜증나네요 목소리" → ['아', '더', '빙', '진짜', '짜증', '나', '네요', '목소리'] ← 4.3 토큰화 (이번 편) → [20, 57, 881, 26, ...] → 벡터 ← 4.4 nn.Embedding → LSTM → 긍정 / 부정 ← 4.5 첫 단계는 문장을 어디서 자를지 정하는 것 이다. 1. 토큰화란 주어진 텍스트(코퍼스)를 토큰(token) 이라는 단위로 나누는 작업을 토큰화(tokenization) 라고 한다. 토큰이 단어면 단어 토큰화 , 문장이면 문장 토큰화 다. 가장 단순한 방법은 구두점을 지우고 띄어쓰기로 자르는 것 이다. 입력: Time is an illusion. Lunchtime double so! 출력: Time / is / an / illusion / Lunchtime / double / so 이 문장은 이걸로 충분하다. 하지만 실제 문장에서는 이 방법이 금방 막힌다. 구두점을 지우면 안 되는 경우가 있고, 띄어쓰기만으로 단어가 나뉘지 않는 언어도 있다. 이번 편은 그 경우들을 하나씩 본다. 2. 같은 문장, 다른 토큰 — 아포스트로피 Don't be fooled by the dark sounding name, Mr. Jone's Orphanage is as cheery as cheery goes for a pastry shop. Don't 와 Jone's 를 어떻게 자를까? 가능한 답이 여러 개다. 원문 가능한 토큰화 Don't Don't / Don t / Dont / Do n't Jone's Jone's / Jone s / Jone / Jones 도구마다 고르는 답이 다르다. 세 가지를 같은 문장에 돌려 보면 이렇다. from nltk.tokenize import word_tokenize, WordPunctTokenizer from tensorflow.keras.preprocessing.text import text_to_word_sequence word_tokenize(s) # 25개 WordPunctTokenizer().tokenize(s) # 28개 text_to_word_sequence(s) # 21개 word_tokenize : n't (not), 's (소유격)처럼 뜻이 있는 단위 로 뗀다. 영어 문법을 아는 토크나이저다. WordPunctTokenizer : 구두점을 무조건 따로 뗀다. 그래서 Don , ' , t 처럼 뜻 없는 조각이 생긴다. text_to_word_sequence : 소문자로 바꾸고 구두점을 지운 뒤 띄어쓰기로 자른다. 아포스트로피만 남겨서 don't , jone's 가 통째로 한 토큰이다. 정답은 없다. 토큰화는 문제에 맞는 단위를 고르는 일 이다. 3. 토큰화에서 고려할 것들 구두점과 특수 문자를 함부로 지우면 안 된다 예 기호가 하는 일 Ph.D , AT&T , m.p.h 마침표·앰퍼샌드가 단어의 일부다 $45.55 마침표를 기준으로 자르면 45 와 55 로 쪼개진다 01/02/06 슬래시가 날짜를 만든다 123,456,789 쉼표가 숫자의 자릿수를 나눈다 1절의 "구두점을 지우고 띄어쓰기로 자르기"는 이런 토큰들을 다 망가뜨린다. 줄임말과 단어 안의 띄어쓰기 줄임말 : I'm 의 'm 처럼 아포스트로피 뒤에 붙는 조각을 접어(clitic) 라고 한다. we're = we are 이다. 띄어쓰기가 있어도 한 단어 인 경우: New York , rock 'n' roll . 표준이 하나 있다 — 펜 트리뱅크 펜 트리뱅크(Penn Treebank) 토큰화 규칙의 핵심은 두 가지다. 하이픈으로 이어진 단어는 하나로 둔다. home-based 아포스트로피 줄임말은 뗀다. doesn't → does + n't from nltk.tokenize import TreebankWordTokenizer TreebankWordTokenizer().tokenize( "Starting a home-based restaurant may be an ideal. it doesn't have a food chain or restaurant of their own.") # ['Starting', 'a', 'home-based', 'restaurant', 'may', 'be', 'an', 'ideal.', 'it', 'does', "n't", ..., 'own', '.'] 🔍 ideal. 의 마침표는 안 떨어졌다. 트리뱅크 토크나이저는 문자열 맨 끝의 마침표만 뗀다. 문장이 하나 들어온다고 가정하기 때문이다. 그래서 보통은 문장 토큰화를 먼저 하고, 문장마다 단어 토큰화를 한다. word_tokenize 는 안에서 이 순서대로 처리한다. 4. 문장 토큰화 — 마침표는 문장 끝이 아니다 문장을 자르는 기준은 보통 . , ? , ! 이다. ? 와 ! 는 비교적 분명하지만, 마침표는 문장 끝이 아닐 때가 많다. IP 주소, 이메일, 약어 속의 마침표에서 자르면 문장이 엉뚱하게 쪼개진다. nltk의 sent_tokenize 는 이런 경우를 학습해 두었다. from nltk.tokenize import sent_tokenize sent_tokenize("I am actively looking for Ph.D. students. and you are a Ph.D student.") # ['I am actively looking for Ph.D. students.', 'and you are a Ph.D student.'] Ph.D. 의 마침표에서는 자르지 않고, students. 에서만 잘랐다. 한국어는 KSS(Korean Sentence Splitter) 를 쓴다. import kss kss.split_sentences('딥 러닝 자연어 처리가 재미있기는 합니다. 그런데 문제는 영어보다 한국어로 할 때 너무 어렵습니다. 이제 해보면 알걸요?') # ['딥 러닝 자연어 처리가 재미있기는 합니다.', '그런데 문제는 영어보다 한국어로 할 때 너무 어렵습니다.', '이제 해보면 알걸요?'] 5. 한국어 토큰화가 어려운 이유 교착어 — 조사가 붙는다 영어는 띄어쓰기 단위가 거의 단어다. 한국어에서 띄어쓰기 단위는 어절 이다. 어절에는 단어에 조사 가 붙어 있다. 이렇게 단어에 조사·어미가 붙어서 문법 기능을 하는 언어를 교착어 라고 한다. 위 그림의 1은 대명사 '그'다. 어절로 자르면 그가 , 그에게 , 그를 , 그와 , 그는 이 전부 다른 단어 가 된다. 모델 입장에서는 이 다섯 개가 서로 관계없는 토큰이다. 그래서 한국어는 형태소 단위로 잘라서 '그'를 떼어 낸다. 형태소 — 뜻을 가진 가장 작은 말 단위 뜻 예 (에디가 책을 읽었다) 자립 형태소 혼자 쓰일 수 있다. 명사, 대명사, 수사, 관형사, 부사, 감탄사 에디, 책 의존 형태소 다른 형태소와 붙어야 쓰인다. 조사, 어미, 접사, 어간 -가, -을, 읽-, -었, -다 용언의 어간 읽- 도 혼자서는 못 쓰기 때문에 의존 형태소다. 띄어쓰기가 잘 안 지켜진다 제가이렇게띄어쓰기를전혀하지않고글을썼다고하더라도글을이해할수있습니다. 한국어는 띄어쓰기를 안 해도 꽤 읽힌다. 글자를 모아 쓰기 때문이다. 영어는 Tobeornottobethatisthequestion 처럼 붙여 쓰면 읽기 어렵다. 그래서 한국어 데이터는 띄어쓰기가 틀린 경우가 많다. 띄어쓰기 기준 토큰화는 더 믿을 수 없다. 4.5의 실제 리뷰에 너무재밓었다그래서보는것을추천한다 가 있다. 형태소 분석기는 이 리뷰를 너무 / 재 / 밓었다그래서보는것을추천한다 로 잘랐다. 오타와 붙여 쓰기 때문에 뒷부분이 통째로 한 토큰이 됐다. 진짜 데이터는 이렇게 지저분하다. 6. 품사 태깅 같은 글자도 품사에 따라 뜻이 다르다. 영어 fly : 동사면 '날다', 명사면 '파리' 한국어 못 : 명사면 '망치로 박는 못', 부사면 '~하지 못하다' 그래서 토큰마다 품사를 붙이는 품사 태깅(part-of-speech tagging) 을 하기도 한다. from nltk.tag import pos_tag pos_tag(word_tokenize("I am actively looking for Ph.D. students. and you are a Ph.D. student.")) # [('I', 'PRP'), ('am', 'VBP'), ('actively', 'RB'), ('looking', 'VBG'), ('for', 'IN'), # ('Ph.D.', 'NNP'), ('students', 'NNS'), ('.', '.'), ...] 태그 뜻 태그 뜻 PRP 인칭 대명사 IN 전치사 VBP 동사 (현재형) NNP 고유 명사 VBG 동사 (현재분사) NNS / NN 복수 / 단수 명사 RB 부사 DT 한정사 4.1의 입출력 구조로 보면 다 대 다 다. 토큰마다 태그가 하나씩 나온다. 7. 한국어 형태소 분석 — KoNLPy KoNLPy 는 한국어 형태소 분석기를 모아 둔 파이썬 패키지다. Okt, Mecab, Komoran, Hannanum, Kkma(꼬꼬마)가 들어 있고, 메서드 이름은 공통이다. 메서드 하는 일 morphs(text) 형태소 단위로 자른 리스트 pos(text) (형태소, 품사) 튜플 리스트 nouns(text) 명사만 from konlpy.tag import Okt, Kkma okt, kkma = Okt(), Kkma() s = "열심히 코딩한 당신, 연휴에는 여행을 가봐요" okt.morphs(s) # ['열심히', '코딩', '한', '당신', ',', '연휴', '에는', '여행', '을', '가봐요'] kkma.morphs(s) # ['열심히', '코딩', '하', 'ᄂ', '당신', ',', '연휴', '에', '는', '여행', '을', '가보', '아요'] okt.nouns(s) # ['코딩', '당신', '연휴', '여행'] 꼬꼬마가 더 잘게 자른다. 코딩한 → 코딩 / 하 / ᄂ , 가봐요 → 가보 / 아요 . 명사 추출 결과는 둘이 같다. 분석기마다 틀리는 곳이 다르다. Okt는 한 을 조사(Josa)로 태깅했다. 실제로는 하다 의 활용( 하 + ᄂ )이다. 어떤 분석기를 쓸지는 문제에 맞게 고른다. 4.5에서는 Mecab 을 쓴다. 다른 분석기보다 훨씬 빨라서, 리뷰 14만 5천 개를 10초 정도에 자른다. 쓰는 법은 똑같이 mecab.morphs(sentence) 한 줄이다. 📌 실습 팁. nltk 3.9 이상에서는 nltk.download('punkt') 만으로는 word_tokenize 가 LookupError 를 낸다. nltk.download('punkt_tab') 이 필요하다. pos_tag 도 nltk.download('averaged_perceptron_tagger_eng') 가 필요하다. 정리 개념 한 줄 토큰화 텍스트를 모델이 다룰 단위(토큰)로 자른다 정답 없다. 도구마다 규칙이 다르고 문제에 맞게 고른다 영어의 난점 아포스트로피( n't , 's ), 기호가 든 토큰( Ph.D , $45.55 ) 트리뱅크 규칙 하이픈은 유지, 줄임말은 분리, 끝 마침표만 분리 문장 토큰화 마침표 ≠ 문장 끝. sent_tokenize , 한국어는 kss 교착어 한국어는 어절에 조사가 붙는다 → 형태소로 잘라야 같은 단어가 모인다 형태소 자립(명사 등) / 의존(조사·어미·접사·어간) 품사 태깅 토큰마다 품사를 붙인다 ( pos_tag , okt.pos ) KoNLPy morphs / pos / nouns . 4.5는 Mecab 말로 설명할 때의 한 문장 "토큰화는 문장을 모델에 넣을 단위로 자르는 일이고 정답은 없습니다. 영어는 아포스트로피와 마침표 처리가 도구마다 다르고, 한국어는 조사가 붙는 교착어라서 띄어쓰기 대신 형태소 분석기로 잘라야 '그가'와 '그를'이 같은 '그'로 모입니다." 다음 편은 이렇게 자른 토큰을 숫자 벡터로 바꾸는 nn.Embedding 이다.
JSP 화면 구조 확장과 차트 데이터 연동 정리 구현 배경 오늘은 프리뷰 화면을 JSP·컨트롤러·CSS·JavaScript로 분리한 작업부터 투자 관련 화면과 차트 연동을 확장한 변경까지 정리했다. 이번 비교 범위에는 텍스트 파일 155개의 변경이 포함되어 있다. 커밋 기록에는 봇 거래 화면, 투자 계획과 투자 기록, 계좌 추가, 일간·섹터 리포트, 모바일 UI 조정 등이 남아 있다. 다만 제공된 diff는 차트 코드 중간에서 잘려 있다. 프로젝트 메모리에도 구체적인 설계 내용은 아직 없어, 전체 기능의 동작을 단정하기보다는 파일과 커밋으로 확인되는 변경 범위 와 코드에서 직접 확인한 로직 을 나누어 기록했다. 변경 구조 변경 파일에서는 다음과 같은 구성을 확인할 수 있었다. 영역 변경에 포함된 구성 서버 화면·API 컨트롤러, 서비스, 리포지터리, 요청 객체와 모델 화면 기능별 JSP, 공통 헤더·푸터, 리포트 조각 JSP 프런트엔드 공통·페이지별 CSS, 페이지 스크립트, 모달·사이드바·스위치 스크립트 차트 HTML 진입 화면, 심볼 검색·조회 JSP, 데이터피드, 레이아웃 저장·조회 코드 데이터·설정 스키마 초기화 클래스, 스키마·시드 SQL, 데이터소스 설정, 비밀 설정 예제 봇, 계좌, 리포트 영역에는 컨트롤러·서비스·리포지터리 파일이 함께 포함되어 있다. 다만 파일 목록만으로 각 계층의 실제 호출 관계나 트랜잭션 경계까지 확인할 수는 없다. 차트 쪽은 공개된 diff에서 조금 더 구체적인 흐름을 볼 수 있었다. 브라우저의 데이터피드 객체가 JSP 엔드포인트를 호출하고, 응답을 차트 라이브러리의 콜백으로 전달하는 방식이다. 핵심 로직 심볼 검색과 시계열 조회 분리 범용 차트 화면의 데이터피드에는 역할별 메서드가 나뉘어 있다. onReady : 지원 주기와 검색 필터 설정 전달 searchSymbols : 입력어와 필터로 심볼 검색 resolveSymbol : 선택한 심볼의 상세 정보 조회 getBars : 심볼·주기·조회 구간으로 시계열 데이터 요청 검색 조건과 시계열 조회 조건은 URLSearchParams 로 구성해 POST 요청으로 보낸다. 지원 주기에는 일·주·월·연 단위가 설정되어 있고, 범용 차트에는 심볼 검색 요청 지연값으로 500ms가 지정되어 있다. 이는 설정값이며, 실제 응답 시간이나 성능 개선을 측정한 결과는 아니다. 조회 결과를 세 가지 상태로 전달 getBars 는 서버 응답 상태에 따라 차트 콜백을 다르게 호출한다. if (data.status == "success") { onHistoryCallback(data.data, { noData: false }); } else if (data.status == "nextData") { onHistoryCallback([], { nextTime: data.nextTime }); } else { onHistoryCallback([], { noData: true }); } 데이터가 있는 경우, 다음 조회 시점이 있는 경우, 데이터가 없는 경우를 구분한 구조다. 요청이나 JSON 처리 중 예외가 발생하면 별도의 오류 콜백으로 전달한다. 다만 마지막 분기는 모든 나머지 상태를 데이터 없음으로 처리한다. 서버가 어떤 상태값을 반환하는지는 공개된 범위만으로 확인되지 않아, 업무 오류까지 빈 결과로 표시되는지 추가 검증이 필요하다. 차트 레이아웃 저장과 복원 차트 위젯의 저장·불러오기 어댑터에는 목록 조회, 삭제, 저장, 개별 내용 조회가 연결되어 있다. 저장할 때는 레이아웃 ID 유무에 따라 신규 저장과 수정을 구분한다. 기존 ID가 객체 형태인지도 확인하고, 최종적으로 문자열로 변환한다. 여기에 저장 시각과 사용자·메뉴 구분 정보를 붙이고 차트 내용을 JSON 문자열로 직렬화한다. 목록 조회에서는 응답의 타임스탬프를 1,000으로 나누는 변환이 있다. 저장 시각은 밀리초로 생성하므로, 서버와 차트 어댑터 사이의 시간 단위를 함께 확인할 필요가 있다. 또한 선택한 심볼이 여러 개라면 첫 번째 심볼을 기본으로 사용하고, 차트 준비 완료 후 나머지를 오버레이로 추가한다. 사용 기술 이번 변경에서 확인한 기술과 구성은 다음과 같다. Java·JSP : 서버 코드와 화면, 차트 연동 엔드포인트 JavaScript·HTML·CSS : 차트 초기화, API 호출, 화면 구성 Fetch API·URLSearchParams·JSON : 폼 요청과 JSON 요청·응답 처리 차트 라이브러리의 데이터피드·저장 어댑터 API : 심볼 검색, 시계열 조회, 레이아웃 관리 TypeScript·Rollup : UDF 데이터피드 소스와 빌드 설정 파일 Maven·SQL : Java 빌드 구성과 스키마·시드 파일 구체적인 라이브러리 버전과 빌드·배포 성공 여부는 제공된 자료로 확인하지 못했다. 고려한 점/문제 해결 이번 기록에는 재현 과정이나 테스트 결과가 포함되어 있지 않다. 따라서 해결을 완료했다고 쓰기보다는, 구현 상태에서 확인한 경계와 추가 검증할 점을 남겼다. 실시간 관련 이름과 실제 구독 구현 구분 커밋에는 실시간 차트 변경이 있고, 일부 요청에는 실시간 여부를 나타내는 값이 포함되어 있다. 하지만 공개된 두 차트 화면의 subscribeBars 와 unsubscribeBars 는 본문이 비어 있다. 이 화면들에서 지속적인 실시간 갱신이 구현되었다고 단정할 수는 없다. 잘린 파일의 나머지 코드와 별도 데이터피드 구현을 확인해야 한다. 요청별 오류 처리 차이 심볼 상세 조회는 response.ok 를 검사하지만, 공개된 시계열 조회와 레이아웃 요청에는 같은 검사가 보이지 않는다. 일부 저장·조회 요청은 예외를 로그로 남기는 데 그친다. 추가 검증할 점 은 HTTP 오류, JSON 파싱 오류, 서버의 업무 오류가 각각 차트와 사용자 화면에 어떻게 전달되는지다. 저장 목록의 반복 조회 레이아웃 목록 조회는 응답 항목을 내부 배열에 계속 추가한다. 공개된 메서드 안에는 배열을 초기화하는 코드가 없다. 반복 호출 시 중복 항목이 쌓이는지, 그리고 해당 내부 배열이 실제 화면 표시에 사용되는지 확인할 필요가 있다. 현재 자료만으로 중복 표시 문제가 발생했다고 말할 수는 없다. 비밀 설정 제외 범위 .gitignore 에는 비밀 설정 파일과 개인 키 확장자를 제외하는 규칙이 추가되었다. 동시에 모든 .properties 파일과 빌드 설정 파일을 제외하는 규칙도 포함되어 있다. 추가로 확인할 항목은 다음과 같다. 이미 추적 중인 민감 파일이 남아 있는지 필요한 공통 설정까지 제외되는지 비밀값 없이도 빌드를 재현할 수 있는지 제외 규칙 추가만으로 기존 Git 이력의 민감 정보까지 제거되었다고 볼 수는 없다. 배운 점 이번 자료에는 시행착오나 검증 결과가 없어, 특정 문제를 해결하며 무엇을 배웠다고 단정하지 않았다. 대신 다음 확인 작업을 남긴다. 데이터피드 계약 : 상태값과 nextTime 의 의미·시간 단위 확인 레이아웃 왕복 저장 : 신규 저장, 즉시 재저장, 불러온 뒤 수정하는 흐름 확인 실시간 갱신 : 구독·해제 구현 위치와 화면 전환 시 정리 여부 확인 모바일 UI : 커밋에 기록된 조정이 실제 화면 크기별로 동작하는지 확인 설정 관리 : 공개 가능한 기본 설정과 비밀 설정의 경계 확인 오늘은 화면 확장 범위를 정리하고, 차트의 검색·조회·저장 흐름을 코드 기준으로 살펴봤다. 다음에는 이 흐름을 실제 요청과 화면 동작으로 검증해, 구현된 부분과 아직 확인이 필요한 부분을 더 명확히 구분하려고 한다.
예전에 노션에 작성했던 문제 해결 과정 옮기는중이다 .. LAG·LEAD 계산 범위 조정을 통한 이전·다음 항목 조회 개선 상세 화면에서 LAG와 LEAD로 이전·다음 항목을 조회했으나, 인접 항목 정보가 정상적으로 조회되지 않았습니다. 윈도우 함수를 계산하는 쿼리 내부에서 특정 ID를 먼저 필터링하여 계산 대상이 한 행으로 제한되었습니다. 따라서 이전·다음 행이 윈도 함수의 계산 범위에서 제외되었습니다. 내가 적용한 해결 방식 내부 서브쿼리에서는 목록에 포함될 공통 조건과 정렬 기준을 적용하여 이전·다음 항목을 계산하고, 외부 쿼리에서는 특정 ID에 해당하는 결과만 선택하도록 수정했습니다. 핵심 코드 또는 원칙 실제 테이블 대신 가상 데이터로 재구성한 SQL 예시입니다. WITH sample_items (item_id) AS ( VALUES (10), (20), (30) ), navigation AS ( SELECT item_id, LAG(item_id) OVER ( ORDER BY item_id ) AS previous_id, LEAD(item_id) OVER ( ORDER BY item_id ) AS next_id FROM sample_items ) SELECT item_id, previous_id, next_id FROM navigation WHERE item_id = 20; -- 결과 -- item_id = 20 -- previous_id = 10 -- next_id = 30 목록 범위를 결정하는 조건은 윈도 함수 계산 전에 적용합니다. 특정 상세 항목을 선택하는 조건은 계산 후 외부에서 적용합니다. 윈도 함수의 계산 범위와 상세 조회 조건을 분리하여 이전·다음 항목 정보가 누락되는 쿼리 구조를 개선했습니다. ** PostgreSQL 시스템 뷰를 활용한 트랜잭션 락 진단** 문제 상황 트랜잭션 실행 중 락으로 인해 작업이 대기하는 상황에서 관련 세션과 실행 상태를 확인할 필요가 있었습니다. 작업 지연 현상만으로는 락 대기 여부와 관련 세션을 파악하기 어려웠습니다. 락 획득 상태와 세션 정보를 함께 조회하여 조사 범위를 좁혀야 했습니다. 원문에는 락을 유지하게 된 개별 트랜잭션의 최종 원인이나 조치 결과가 기록되어 있지 않아, 이 사례는 진단 과정 중심으로 정리했습니다. pg_locks와 pg_stat_activity를 PID 기준으로 연결하여 락 유형, 모드, 획득 여부와 세션의 실행 SQL·시작 시각·상태를 확인하는 조회 SQL을 정리했습니다. 대기 중인 락과 특정 대상의 관련 PID를 확인하는 조건을 구분하여, 상황에 따라 조회 범위를 좁힐 수 있도록 구성했습니다. 핵심 코드 또는 원칙 아래는 원문의 조회 방향을 단순화한 예시입니다. 업무 테이블명이나 실제 SQL 내용이 결과에 노출되지 않도록 대상 테이블명과 query 컬럼을 제외했습니다. pg_locks와 pg_stat_activity는 PostgreSQL 표준 시스템 뷰입니다. SELECT a.pid, l.locktype, l.mode, l.granted, a.state, a.query_start FROM pg_locks AS l JOIN pg_stat_activity AS a ON a.pid = l.pid WHERE l.granted = false ORDER BY a.query_start; granted = false는 락 획득을 기다리는 상태입니다. 대기 세션을 락 보유 세션으로 단정하지 않습니다. 조회 자체는 락을 해제하지 않으며, 상태 확인과 세션 종료는 별도 조치입니다.
Biz Assist에 외부 공고 수집처를 추가하면서 BidCandidateCollector 인터페이스를 만들었다. public interface BidCandidateCollector { List<BidQualificationDto> collect( LocalDate startDate, LocalDate endDate ); } interface가 뭐였지 인터페이스는 클래스가 어떤 기능을 가져야 하는지 정하는 약속 이다. 위 코드에서는 collect(startDate, endDate) 라는 메서드를 구현해야 한다. 즉 수집처가 어디든 기간을 받아서 공고 후보 목록을 반환해야 한다. implements 인터페이스를 실제 클래스에서 구현할 때는 implements 를 사용한다. class KoreaExpresswayBidCollector implements BidCandidateCollector { } 그리고 인터페이스에 정의된 메서드를 실제로 구현한다. @Override public List<BidQualificationDto> collect( LocalDate startDate, LocalDate endDate ) { // 수집 로직 } Biz Assist에서는 왜 썼나 나라장터와 한국도로공사는 공고를 가져오는 방식이 다르다. 그래도 Biz Assist 입장에서는 둘 다 공고 후보를 가져오는 수집기 다. 그래서 실제 구현은 각 클래스에서 다르게 하고 밖에서는 같은 BidCandidateCollector 타입으로 다룰 수 있게 했다. 나라장터 수집기 한국도로공사 수집기 다른 수집처 ↓ 모두 BidCandidateCollector 다시 정리 interface → 해야 할 기능의 형태 정의 implements → 인터페이스를 실제 클래스에서 구현 @Override → 정의된 메서드를 실제 동작으로 작성 이번에 직접 써보니까 interface는 구현 방식은 다르지만 같은 역할을 하는 클래스들을 묶는 기준 이라고 생각하면 이해하기 쉬웠다.
네이버 영화 리뷰 데이터에 대한 이해와 전처리 LSTM을 이용한 네이버 영화 리뷰 분류 모델 평가 코드 작성 학습 모델 로드 및 평가 모델 테스트 <딥러닝 파이토치 교과서 - 입문부터 LLM 파인튜닝까지> 교재 13-2장의 내용을 공부하고 정리한 포스트입니다. https://wikidocs.net/book/2788 - 교재 https://github.com/ukairia777/pytorch-nlp-tutorial - 깃허브 이번 주에 배운 걸 전부 합쳐보자. 4.3의 형태소 토큰화로 리뷰를 나누고, 정수 인코딩한 뒤 4.4의 nn.Embedding() 으로 벡터로 바꾸고, 4.2의 LSTM에 넣어 긍정/부정을 맞히는 모델을 만든다. 4.1에서 본 다 대 일 (many-to-one) 구조다. 데이터는 네이버 영화 리뷰 데이터다. 총 200,000개 리뷰로, 리뷰 텍스트와 긍정이면 1, 부정이면 0인 레이블로 구성돼 있다. 1. 네이버 영화 리뷰 데이터에 대한 이해와 전처리 데이터 다운로드 링크 : https://github.com/e9t/nsmc/ import pickle import pandas as pd import numpy as np import matplotlib.pyplot as plt import re import urllib.request from konlpy.tag import Mecab from tqdm import tqdm from sklearn.model_selection import train_test_split from collections import Counter 1) 데이터 로드하기 훈련 데이터 ratings_train.txt와 테스트 데이터 ratings_test.txt를 다운로드한다. urllib.request.urlretrieve("https://raw.githubusercontent.com/e9t/nsmc/master/ratings_train.txt", filename="ratings_train.txt") urllib.request.urlretrieve("https://raw.githubusercontent.com/e9t/nsmc/master/ratings_test.txt", filename="ratings_test.txt") pandas로 훈련 데이터는 train_data 에, 테스트 데이터는 test_data 에 저장한다. train_data = pd.read_table('ratings_train.txt') test_data = pd.read_table('ratings_test.txt') print('훈련용 리뷰 개수 :',len(train_data)) # 훈련용 리뷰 개수 출력 훈련용 리뷰 개수 : 150000 훈련 데이터는 150,000개다. 상위 5개를 출력해보자. train_data[:5] # 상위 5개 출력 id, document, label 3개 열로 구성돼 있다. id는 감성 분류에 도움이 안 되니 무시한다. 결국 모델은 리뷰 내용인 document와 긍정(1)/부정(0)인 label 두 열을 학습해야 한다. 상위 5개만 봐도 한국어 데이터의 특징이 보인다. 2번 샘플은 4.3에서 본 것처럼 띄어쓰기를 안 해도 이해되는 한국어 특성 때문에 띄어쓰기가 안 돼 있다. 테스트 데이터도 확인하자. print('테스트용 리뷰 개수 :',len(test_data)) # 테스트용 리뷰 개수 출력 테스트용 리뷰 개수 : 50000 test_data[:5] 테스트 데이터는 50,000개이고 형식은 훈련 데이터와 같다. 2) 데이터 정제하기 훈련 데이터에 중복이 있는지 확인한다. # document 열과 label 열의 중복을 제외한 값의 개수 train_data['document'].nunique(), train_data['label'].nunique() (146182, 2) 150,000개 중 document 열의 고유값이 146,182개라는 건 약 4,000개가 중복이라는 뜻이다. label은 0 또는 1이라 2가 나온다. 중복을 제거하자. # document 열의 중복 제거 train_data.drop_duplicates(subset=['document'], inplace=True) print('총 샘플의 수 :',len(train_data)) 총 샘플의 수 : 146183 146,182가 아니라 146,183인 이유는 nunique() 가 Null을 세지 않기 때문이다. 아래에서 보는 Null 샘플 1개가 남아 있다. 레이블 분포를 보자. train_data['label'].value_counts().plot(kind = 'bar') 긍정과 부정 모두 약 72,000개로 분포가 균일해 보인다. 정확한 개수를 확인하자. print(train_data.groupby('label').size().reset_index(name = 'count')) label count 0 0 73342 1 1 72841 레이블 0인 리뷰가 근소하게 많다. Null 값을 가진 샘플이 있는지 확인한다. print(train_data.isnull().values.any()) True Null이 있다. 어느 열에 있는지 보자. print(train_data.isnull().sum()) id 0 document 1 label 0 dtype: int64 document 열에 1개 있다. 어떤 샘플인지 출력해보자. train_data.loc[train_data.document.isnull()] Null 샘플을 제거한다. train_data = train_data.dropna(how = 'any') # Null 값이 존재하는 행 제거 print(train_data.isnull().values.any()) # Null 값이 존재하는지 확인 False print(len(train_data)) 146182 1개가 제거됐다. 이제 온점(.)이나 ? 같은 특수문자를 지우고 한글만 남기기 위해 정규 표현식을 쓴다. 영어로 먼저 보자. 알파벳을 나타내는 정규 표현식은 [a-zA-Z] 다. 이를 응용하면 알파벳과 공백을 제외하고 모두 지울 수 있다. #알파벳과 공백을 제외하고 모두 제거 eng_text = 'do!!! you expect... people~ to~ read~ the FAQ, etc. and actually accept hard~! atheism?@@' print(re.sub(r'[^a-zA-Z ]', '', eng_text)) 'do you expect people to read the FAQ etc and actually accept hard atheism' 한국어도 같은 원리다. 한글 범위만 정하면 된다. 자음은 ᄀ ~ ᄒ, 모음은 ᅡ ~ ᅵ로 지정한다. 자모 참고 : https://www.unicode.org/charts/PDF/U3130.pdf ᄀ ~ ᄒ : 3131 ~ 314E ᅡ ~ ᅵ : 314F ~ 3163 완성형 한글은 가 ~ 힣으로 지정한다. 완성형 참고 : https://www.unicode.org/charts/PDF/UAC00.pdf 이 범위를 모두 반영해 한글과 공백을 제외하고 전부 제거하자. # 한글과 공백을 제외하고 모두 제거 train_data['document'] = train_data['document'].str.replace("[^ᄀ-하-ᅵ가-힣 ]","", regex=True) train_data[:5] 띄어쓰기는 유지되고 구두점은 제거됐다. 그런데 네이버 영화 리뷰는 영어, 숫자, 특수문자로만 쓸 수도 있다. 한글이 없던 리뷰는 이제 빈(empty) 값이 됐을 것이다. 공백만 있거나 빈 값인 행을 Null로 바꾸고 확인하자. train_data['document'] = train_data['document'].str.replace('^ +', "", regex=True) # white space 데이터를 empty value로 변경 train_data['document'].replace('', np.nan, inplace=True) print(train_data.isnull().sum()) id 0 document 789 label 0 dtype: int64 Null이 789개나 새로 생겼다. 5개만 보자. train_data.loc[train_data.document.isnull()][:5] 레이블은 긍정일 수도 부정일 수도 있지만, 텍스트가 없으니 의미 없는 데이터다. 제거한다. train_data = train_data.dropna(how = 'any') print(len(train_data)) 145393 145,393개가 남았다. 테스트 데이터에도 같은 전처리를 한다. test_data.drop_duplicates(subset = ['document'], inplace=True) # document 열에서 중복인 내용이 있다면 중복 제거 test_data['document'] = test_data['document'].str.replace("[^ᄀ-하-ᅵ가-힣 ]","", regex=True) # 정규 표현식 수행 test_data['document'] = test_data['document'].str.replace('^ +', "", regex=True) # 공백은 empty 값으로 변경 test_data['document'].replace('', np.nan, inplace=True) # 공백은 Null 값으로 변경 test_data = test_data.dropna(how='any') # Null 값 제거 print('전처리 후 테스트용 샘플의 개수 :',len(test_data)) 전처리 후 테스트용 샘플의 개수 : 48852 3) 토큰화 토큰화하면서 불용어 (stopwords)도 제거한다. 불용어는 정의하기 나름이다. 조사, 접속사 같은 보편적인 불용어를 쓸 수도 있지만, 결국 데이터를 계속 검토하면서 추가해가는 경우가 많다. 현업이라면 보통 아래보다 더 많은 불용어를 쓴다. stopwords = ['도', '는', '다', '의', '가', '이', '은', '한', '에', '하', '고', '을', '를', '인', '듯', '과', '와', '네', '들', '듯', '지', '임', '게'] 형태소 분석기는 KoNLPy의 Mecab을 쓴다. mecab = Mecab() mecab.morphs('와 이런 것도 영화라고 차라리 뮤직비디오를 만드는 게 나을 뻔') ['와', '이런', '것', '도', '영화', '라고', '차라리', '뮤직', '비디오', '를', '만드', '는', '게', '나을', '뻔'] 4.3에서 본 것처럼 한국어는 띄어쓰기가 아니라 형태소 분석기로 토큰화한다. 훈련 데이터를 토큰화하면서 불용어를 제거해 X_train 에 저장한다. X_train = [] for sentence in tqdm(train_data['document']): tokenized_sentence = mecab.morphs(sentence) # 토큰화 stopwords_removed_sentence = [word for word in tokenized_sentence if not word in stopwords] # 불용어 제거 X_train.append(stopwords_removed_sentence) print(X_train[:3]) [['아', '더', '빙', '진짜', '짜증', '나', '네요', '목소리'], ['흠', '포스터', '보고', '초딩', '영화', '줄', '오버', '연기', '조차', '가볍', '않', '구나'], ['너무', '재', '밓었다그래서보는것을추천한다']] 세 번째 샘플처럼 띄어쓰기가 안 된 데다 오타('밓었다')까지 있으면 형태소 분석기도 제대로 못 나눈다. 테스트 데이터도 똑같이 토큰화한다. X_test = [] for sentence in tqdm(test_data['document']): tokenized_sentence = mecab.morphs(sentence) # 토큰화 stopwords_removed_sentence = [word for word in tokenized_sentence if not word in stopwords] # 불용어 제거 X_test.append(stopwords_removed_sentence) 4) 학습 데이터, 검증 데이터, 테스트 데이터 학습 중 성능을 평가할 검증 데이터 가 추가로 필요하다. 레이블 열을 따로 분리해 y_train , y_test 로 저장한다. 그러면 학습 데이터는 X_train , y_train , 테스트 데이터는 X_test , y_test 가 된다. 학습 데이터의 20%를 떼어 검증 데이터를 만든다. 데이터 분리는 주로 사이킷런의 train_test_split 을 쓰고, test_size 에 비율을 넣으면 그만큼 분할해준다. 랜덤 분할 중 레이블 불균형이 생기지 않도록 레이블 비율을 유지하고 싶다면 stratify 에 y 데이터를 넣는다. y_train = np.array(train_data['label']) y_test = np.array(test_data['label']) X_train, X_valid, y_train, y_valid = train_test_split(X_train, y_train, test_size=0.2, random_state=0, stratify=y_train) 비율이 잘 유지됐는지 확인하자. print('--------학습 데이터의 비율-----------') print(f'부정 리뷰 = {round(np.sum(y_train==0)/len(y_train) * 100,3)}%') print(f'긍정 리뷰 = {round(np.count_nonzero(y_train)/len(y_train) * 100,3)}%') print('--------검증 데이터의 비율-----------') print(f'부정 리뷰 = {round(np.sum(y_valid==0)/len(y_valid) * 100,3)}%') print(f'긍정 리뷰 = {round(np.count_nonzero(y_valid)/len(y_valid) * 100,3)}%') print('--------테스트 데이터의 비율-----------') print(f'부정 리뷰 = {round(np.sum(y_test==0)/len(y_test) * 100,3)}%') print(f'긍정 리뷰 = {round(np.count_nonzero(y_test)/len(y_test) * 100,3)}%') --------학습 데이터의 비율----------- 부정 리뷰 = 50.238% 긍정 리뷰 = 49.762% --------검증 데이터의 비율----------- 부정 리뷰 = 50.239% 긍정 리뷰 = 49.761% --------테스트 데이터의 비율----------- 부정 리뷰 = 49.808% 긍정 리뷰 = 50.192% 학습 데이터와 검증 데이터의 레이블 비율이 같다. 5) 단어 집합 만들기 텍스트를 숫자로 처리하려면 정수 인코딩을 해야 한다. 먼저 훈련 데이터로 단어 집합 (vocabulary)을 만든다. word_list = [] for sent in X_train: for word in sent: word_list.append(word) word_counts = Counter(word_list) print('총 단어수 :', len(word_counts)) 총 단어수 : 45296 단어가 45,000개가 넘는다. Counter() 로 셌기 때문에 각 단어의 등장 빈도가 저장돼 있다. print('훈련 데이터에서의 단어 영화의 등장 횟수 :', word_counts['영화']) print('훈련 데이터에서의 단어 공감의 등장 횟수 :', word_counts['공감']) 훈련 데이터에서의 단어 영화의 등장 횟수 : 45791 훈련 데이터에서의 단어 공감의 등장 횟수 : 756 빈도수가 높은 순서로 정렬하자. vocab = sorted(word_counts, key=word_counts.get, reverse=True) print('등장 빈도수 상위 10개 단어') print(vocab[:10]) 등장 빈도수 상위 10개 단어 ['영화', '보', '있', '없', '좋', '나', '었', '만', '는데', '너무'] 빈도수가 낮은 단어는 배제하려고 한다. 등장 빈도가 3회 미만인 단어가 얼마나 되는지 보자. threshold = 3 total_cnt = len(word_counts) # 단어의 수 rare_cnt = 0 # 등장 빈도수가 threshold보다 작은 단어의 개수를 카운트 total_freq = 0 # 훈련 데이터의 전체 단어 빈도수 총 합 rare_freq = 0 # 등장 빈도수가 threshold보다 작은 단어의 등장 빈도수의 총 합 # 단어와 빈도수의 쌍(pair)을 key와 value로 받는다. for key, value in word_counts.items(): total_freq = total_freq + value # 단어의 등장 빈도수가 threshold보다 작으면 if(value < threshold): rare_cnt = rare_cnt + 1 rare_freq = rare_freq + value print('단어 집합(vocabulary)의 크기 :',total_cnt) print('등장 빈도가 %s번 이하인 희귀 단어의 수: %s'%(threshold - 1, rare_cnt)) print("단어 집합에서 희귀 단어의 비율:", (rare_cnt / total_cnt)*100) print("전체 등장 빈도에서 희귀 단어 등장 빈도 비율:", (rare_freq / total_freq)*100) 단어 집합(vocabulary)의 크기 : 45296 등장 빈도가 2번 이하인 희귀 단어의 수: 26105 단어 집합에서 희귀 단어의 비율: 57.63202048746025 전체 등장 빈도에서 희귀 단어 등장 빈도 비율: 2.2769635638286716 2회 이하로 나온 단어가 단어 집합의 절반 이상(57.6%)을 차지하지만, 실제 등장 빈도로는 2.27%밖에 안 된다. 별로 중요하지 않은 단어들이니 정수 인코딩에서 배제한다. 단어 집합의 최대 크기를 이 단어들을 뺀 개수로 제한하자. # 전체 단어 개수 중 빈도수 2이하인 단어는 제거. vocab_size = total_cnt - rare_cnt vocab = vocab[:vocab_size] print('단어 집합의 크기 :', len(vocab)) 단어 집합의 크기 : 19191 단어 집합의 크기는 19,191이다. 여기에 패딩과 모르는 단어를 위해 <PAD> 와 <UNK> 를 추가한다. 실제 의미는 없지만 특별한 용도로 쓰는 이런 단어를 스페셜 토큰 (Special Token)이라고 한다. 각각 0과 1에 할당한다. 4.4에서 <unk> , <pad> 를 단어 집합에 넣었던 것과 같다. word_to_index = {} word_to_index['<PAD>'] = 0 word_to_index['<UNK>'] = 1 for index, word in enumerate(vocab) : word_to_index[word] = index + 2 vocab_size = len(word_to_index) print('패딩 토큰과 UNK 토큰을 고려한 단어 집합의 크기 :', vocab_size) 패딩 토큰과 UNK 토큰을 고려한 단어 집합의 크기 : 19193 print('단어 <PAD>와 맵핑되는 정수 :', word_to_index['<PAD>']) print('단어 <UNK>와 맵핑되는 정수 :', word_to_index['<UNK>']) print('단어 영화와 맵핑되는 정수 :', word_to_index['영화']) 단어 <PAD>와 맵핑되는 정수 : 0 단어 <UNK>와 맵핑되는 정수 : 1 단어 영화와 맵핑되는 정수 : 2 빈도 1위인 '영화'가 스페셜 토큰 바로 다음인 2가 됐다. 6) 정수 인코딩 이제 정수 인코딩을 하자. 단어 집합에 없는 단어는 일괄로 <UNK> , 즉 정수 1로 매핑한다. def texts_to_sequences(tokenized_X_data, word_to_index): encoded_X_data = [] for sent in tokenized_X_data: index_sequences = [] for word in sent: try: index_sequences.ap
컴퓨터를 사용하면서 우리는 운영체제라는 말을 자주 접한다. Windows, macOS, Linux, Android, iOS처럼 익숙한 이름들이 모두 운영체제에 해당한다. 그런데 운영체제는 정확히 어떤 일을 하고 있을까? 이번 글에서는 운영체제가 무엇인지부터 시작해 커널, 사용자 모드와 커널 모드, 시스템 호출 까지 운영체제의 기본 구조를 정리해본다. 1. 운영체제란? 운영체제의 정의 운영체제(OS, Operating System)는 실행할 프로그램에 필요한 자원을 할당하고, 프로그램이 올바르게 실행되도록 돕는 특별한 프로그램 이다. 쉽게 말하면 운영체제는 응용 프로그램과 하드웨어 사이에서 다리 역할을 하는 프로그램 이라고 볼 수 있다. 프로그램이 실행되려면 CPU, 메모리, 저장장치, 입출력장치 등의 자원이 필요하다. 하지만 여러 프로그램이 동시에 실행된다면 문제가 생긴다. 어떤 프로그램에 CPU를 먼저 사용할 수 있게 할까? 메모리는 얼마나 할당해야 할까? 여러 프로그램이 하나의 입출력장치를 사용하면 어떻게 해야 할까? 저장장치의 파일은 어떻게 관리할까? 운영체제는 이러한 자원들을 관리하면서 프로그램이 정상적으로 실행될 수 있도록 돕는다. 운영체제를 한 나라의 자원을 효율적으로 배분하는 정부 에 비유하면 이해하기 쉽다. 국가가 한정된 자원을 필요한 곳에 배분하듯이, 운영체제도 컴퓨터의 한정된 자원을 여러 프로그램에 적절하게 배분한다. 대표적인 운영체제 Windows macOS Linux Android iOS 운영체제는 어디에서 실행될까? 운영체제는 메모리에서 커널 영역 이라는 특별한 공간에 적재되어 실행된다. 반면 일반적인 응용 프로그램은 사용자 영역 에 적재된다. 운영체제의 핵심적인 부분은 커널 영역에서 실행되고, 일반적인 응용 프로그램은 사용자 영역에서 실행된다고 이해하면 된다. 2. 운영체제는 어떤 일을 할까? 운영체제의 핵심 역할은 컴퓨터의 자원을 관리하는 것이다. 1 메모리 자원 관리 실행 중인 프로그램을 메모리의 빈 공간에 적재하고, 더 이상 사용하지 않는 프로그램을 정리하면서 메모리를 관리한다. 즉, 여러 프로그램이 메모리를 효율적으로 사용할 수 있도록 관리하는 역할이다. 2 CPU 자원 할당 CPU는 한정된 자원이다. 여러 프로그램이 동시에 실행되는 환경에서는 어떤 프로그램을 먼저 실행할지, 얼마나 오랫동안 CPU를 사용할지 결정해야 한다. 운영체제는 CPU 스케줄링 을 통해 이러한 작업을 관리한다. 3 입출력장치 관리 프린터와 같은 입출력장치를 여러 프로그램이 동시에 사용하려고 하면 충돌이 발생할 수 있다. 운영체제는 이러한 장치의 사용 순서를 조정하여 문제가 발생하지 않도록 관리한다. 4 파일 시스템 관리 저장장치에 있는 정보를 파일과 폴더(디렉터리) 단위로 관리한다. 우리가 컴퓨터에서 파일을 만들고, 삭제하고, 폴더에 정리할 수 있는 것도 운영체제의 파일 시스템 관리와 관련되어 있다. 3. 개발자가 운영체제를 알아야 하는 이유 운영체제는 컴퓨터를 사용하는 사람뿐만 아니라 개발자에게도 중요한 개념이다. 하드웨어 조작을 대신해준다 개발자가 프로그램을 만들 때 하드웨어를 직접 제어하는 코드를 모두 작성해야 한다면 매우 복잡해질 것이다. 운영체제가 하드웨어에 접근하고 조작하는 작업을 대신 처리하기 때문에 개발자는 운영체제에서 제공하는 기능을 이용할 수 있다. 문제 해결에 도움이 된다 프로그램을 실행하다 보면 메모리 부족이나 파일 접근 문제처럼 다양한 오류를 만날 수 있다. 운영체제는 하드웨어와 가까운 곳에서 프로그램의 실행을 관리하기 때문에 문제 상황을 감지하고 오류 정보를 제공하기도 한다. 따라서 운영체제의 동작 방식을 이해하면 오류가 발생했을 때 단순히 에러 메시지를 보는 것을 넘어, 왜 그런 문제가 발생했는지 추적하는 데 도움 이 된다. 기술 면접에서도 자주 다뤄진다 운영체제는 개발자가 알아야 할 대표적인 CS 기초 지식 중 하나다. 특히 프로세스, 스레드, 메모리, CPU 스케줄링, 동기화 등의 개념은 이후 다른 CS 개념을 이해하는 데에도 연결된다. 4. 운영체제의 핵심, 커널 운영체제는 매우 규모가 큰 프로그램이다. 그중에서도 운영체제의 핵심적인 서비스를 담당하는 부분을 커널(Kernel) 이라고 한다. 커널은 컴퓨터의 자원에 접근하고 조작하는 기능과 프로그램이 안전하게 실행될 수 있도록 관리하는 기능을 담당한다. 쉽게 표현하면, 운영체제에서 가장 핵심적인 일을 담당하는 부분이 커널이다. 운영체제의 종류는 다양하지만, 운영체제의 핵심적인 역할을 담당하는 커널의 기능은 서로 비슷한 부분이 많다. 그래서 전공 서적에서는 운영체제와 커널을 거의 같은 의미로 사용하는 경우도 있다. 5. 사용자와 컴퓨터를 연결하는 UI 운영체제에는 사용자와 컴퓨터가 상호작용할 수 있도록 하는 사용자 인터페이스(UI) 도 존재한다. UI는 크게 두 가지 방식으로 생각할 수 있다. GUI GUI(Graphical User Interface) 는 아이콘이나 마우스 등을 이용해 컴퓨터와 상호작용하는 방식이다. 우리가 평소 Windows나 macOS를 사용할 때 보는 화면이 대표적인 예다. CLI CLI(Command Line Interface) 는 명령어를 직접 입력하여 컴퓨터에 작업을 요청하는 방식이다. 터미널이나 명령 프롬프트를 사용하는 것이 대표적인 예다. GUI → 아이콘, 창, 마우스 등을 이용 CLI → 명령어를 직접 입력 여기서 한 가지 기억해야 할 점이 있다. UI는 운영체제에 포함될 수 있지만, 커널 그 자체는 아니다. 커널은 운영체제의 핵심 기능을 담당하는 부분이고, UI는 사용자가 운영체제와 상호작용할 수 있도록 하는 통로라고 이해하면 된다. 6. 사용자 모드와 커널 모드 운영체제를 이해할 때 중요한 개념 중 하나가 이중 모드 다. 이중 모드란? CPU가 명령어를 실행하는 모드를 크게 사용자 모드 커널 모드 로 구분하는 방식이다. 이렇게 모드를 구분하는 이유는 응용 프로그램이 컴퓨터의 모든 자원에 마음대로 접근하지 못하도록 하기 위해서 다. 사용자 모드 사용자 모드에서는 운영체제의 서비스를 제한적으로 이용할 수 있다. 커널 영역의 코드를 직접 실행할 수 없다. 시스템의 자원에 직접 접근할 수 없다. 즉, 일반적인 응용 프로그램은 사용자 모드에서 실행되며 중요한 자원에 마음대로 접근할 수 없다. 커널 모드 커널 모드에서는 운영체제의 서비스를 제공받을 수 있다. 또한 자원에 접근하는 것을 비롯해 사용자 모드보다 더 많은 명령을 실행할 수 있다. 정리하면 다음과 같다. 구분 사용자 모드 커널 모드 커널 영역 코드 실행 불가능 가능 자원에 대한 접근 제한됨 가능 목적 일반적인 프로그램 실행 운영체제의 핵심 기능 수행 현재 CPU가 어떤 모드에서 실행되고 있는지는 CPU 내부의 플래그 레지스터를 통해 확인할 수 있다. 7. 시스템 호출(System Call) 그렇다면 사용자 모드에서 실행되는 프로그램은 운영체제의 기능을 어떻게 사용할까? 여기서 등장하는 것이 시스템 호출(System Call) 이다. 응용 프로그램이 운영체제의 서비스를 이용하려면 시스템 호출을 통해 운영체제에 요청한다. 즉, 응용 프로그램 ↓ 사용자 모드 ↓ 시스템 호출 ↓ 커널 모드 ↓ 운영체제 기능 수행 ↓ 사용자 프로그램으로 복귀 와 같은 흐름으로 이해할 수 있다. 시스템 호출은 커널 모드로 전환하여 운영체제의 서비스를 요청하기 위해 호출하는 것 이며, 일종의 소프트웨어 인터럽트라고 볼 수 있다. 예를 들어 프로그램이 파일을 읽거나 저장장치에 접근하는 등의 작업을 운영체제에 요청할 때 시스템 호출이 사용될 수 있다. 8. 운영체제가 제공하는 핵심 서비스 지금까지 내용을 조금 더 넓게 보면 운영체제의 핵심 서비스는 크게 세 가지로 정리할 수 있다. 프로세스 관리 운영체제는 실행 중인 프로그램인 프로세스 를 관리한다. 수많은 프로세스가 생성되고 실행되고 종료되기 때문에 운영체제는 이들을 체계적으로 관리해야 한다. 여기에는 다음과 같은 내용이 포함된다. 프로세스와 스레드 관리 프로세스 동기화 교착 상태 해결 자원 접근 및 할당 운영체제는 다양한 컴퓨터 자원을 관리한다. CPU 메모리 입출력장치 예를 들어 CPU를 어떤 프로세스에 먼저 할당할지 결정하는 것이 CPU 스케줄링이다. 파일 시스템 관리 저장장치에 있는 정보를 파일이라는 단위로 관리하고, 파일을 폴더(디렉터리) 단위로 묶어 관리한다. 마무리 운영체제를 처음 공부하면 용어가 많아서 어렵게 느껴질 수 있다. 하지만 핵심을 하나로 정리하면 조금 단순해진다. 운영체제는 프로그램이 컴퓨터의 자원을 안전하고 효율적으로 사용할 수 있도록 관리하는 프로그램이다. 그리고 그 중심에는 커널 이 있다. 응용 프로그램은 사용자 모드에서 실행되고, 운영체제의 기능이 필요할 때 시스템 호출 을 통해 커널에 요청한다. 응용 프로그램 ↓ 사용자 모드 ↓ 시스템 호출 ↓ 커널 ↓ CPU · 메모리 · 입출력장치 · 파일 시스템 관리 다음 글에서는 여기서 한 단계 더 들어가서, 운영체제가 실제로 프로그램을 어떻게 관리하는지 알아본다. 다음 주제는 프로세스와 스레드 다.