컴공 5대과목 후기

오유찬·2026년 6월 20일
post-thumbnail

이번 학기에 컴공 5대 과목이라 불리는 운영체제, 데이터베이스, 컴퓨터구조론, 알고리즘, 네트워크를 한꺼번에 들었다.

컴공의 꽃이라는 이 과목들을 1학년 때부터 듣고 싶었는데, 이제야 들을 수 있어서 기뻤다.

인하대학교의 커리큘럼은 이렇다.

  • 운영체제: 프로세스와 스레드, 컨텍스트 스위칭, CPU 스케줄링, 동기화 도구, 데드락, 메모리 관리, 페이징, 가상 메모리, 파일시스템
  • 컴퓨터구조론: 성능 분석, MIPS 명령어, 데이터 연산, 데이터패스, 파이프라인, 데이터 해저드, 캐시, 가상메모리
  • 데이터베이스: 관계 대수, SQL, 조인, View, ER 모델, 정규화, 파일 구조, 인덱싱, B+트리, 쿼리 프로세싱
  • 네트워크: OSI 7계층, TCP, 흐름제어, 혼잡제어, IP, ARP, ICMP, NAT, Unicast Routing, HTTP
  • 알고리즘: 정렬 알고리즘, 분할 상환 분석, 레드블랙트리, 최소 스패닝 트리, 최단 경로, DP, 문자열 매칭, NP 완전

5개 과목을 동시에 듣다 보니, 서로 다른 과목인데도 공통적으로 사용하는 기법들이 보였다.
OS에서 특정 문제를 풀기 위해 사용한 방법이 다른 과목의 문제를 풀 때도 쓰이고, 내가 프로젝트를 할때 설계했던 것들과도 겹쳐보이는게 많았다.
이렇게 공통적으로 사용하는 몇가지 패턴들이 보여서 이번 기회에 한번 정리해보려고 한다.

그리고 처음에는 개인적으로 cs를 공부한게 있어서 상대적으로 배울게 적을 수 있겠다고 생각했었는데, 막상 과목을 들어보니 더 깊게 다루는 부분이 많고 몰랐던 내용들도 많아서 원래 알고있던 지식은 겉핥기 수준이였다는 것을 알게 되었다.
이렇게 새로 알게된 내용 중 몇가지를 뽑아서 적어보려고 한다.


공통된 패턴

1. 보여주는 주소와 실제로 저장된 주소를 분리하는 패턴

  • OS (가상 메모리): 프로세스는 본인이 연속된 메모리를 쓰는 줄 알지만, 실제로는 페이지 테이블을 통해 물리 메모리 곳곳에 흩어져 있다.
  • DB (블록 주소): 레코드를 직접 물리 주소로 다루지 않고, 페이지/블록 구조와 인덱스를 통해 실제 디스크 위치를 찾아간다.
  • 그림판 (픽셀 주소): 텍스처에 타일은 연속적으로 존재하지 않고, 불연속적으로 있을 수 있다.

이것처럼 실제 데이터가 존재하는 곳이 분리되어있는 패턴은 꽤나 흔하게 사용되는 것 같다.
최근에 고해상도 그림도구를 설계를 하곤 했는데 프레임버퍼에 담긴 텍스쳐 내부에 타일들이 불연속적으로 배치되어있고, 그 위치를 따로 테이블로 관리한다는 점에서 뭔가 결이 비슷하다고 생각했다.
이렇게 주소를 분리하는 것은 아래에 나오는 패턴과도 크게 연관이 있다.


2. 데이터를 일정한 크기 단위로 자르는 패턴

  • OS (page): 메모리를 고정 크기 페이지로 잘라 외부 단편화를 줄인다.
  • DB (block): 디스크 I/O를 줄이기 위해 블록으로 나눈다.
  • 그림판 (밉맵 타일): VRAM에 올릴 데이터를 줄이기 위해 밉맵을 적용해야 했고, 밉맵을 구현하기 위해 타일을 사용한다.

이렇게 한정된 자원을 효율적으로 사용하기 위해 데이터를 특정한 단위로 분할하는 장면이 최근에 많이 보였고, 실제로 설계하는데에 사용하기도 했다.
이렇게 분할을 하면, 어쨌든 기존 데이터 단위로 다루던 작업을 프레임, 블록, 타일별로 생각하는 패러다임의 전환이 일어나기 때문에 추상화하기에도 더 유용하다는 생각이 든다.

기존엔 렌더링, 스냅샷 저장, 비트맵 수정 등의 작업을 비트맵단위로 했지만, 타일로 나누니 개발하는 입장에서 비트맵등의 저수준까지 내려갈 필요도 없고, 고정크기를 가지니깐 관리하기도 편한 장점들이 생겼다. 편집 모듈들은 오직 타일만 받으면 그 타일만 수정하면 되니까 의존성도 분리된다.


3. 자주 쓰는 걸 빠른 곳에 올려두는 패턴

  • 컴구 (CPU 캐시): L1/L2/L3, 지역성을 활용해 느린 메인 메모리 접근을 줄인다.
  • OS (demand paging): 필요할 때 페이지를 메모리로 올리고, 이미 올라온 페이지는 재접근 시 그대로 쓴다.
  • DB (buffer pool): I/O를 줄이기 위해 블록을 메모리에 캐싱해두고 재사용한다.
  • 그림판 (타일): 고해상도 캔버스를 구현한다면, 필요하지 않은 타일을 IndexedDB에 저장할 수 있다.

이런 캐싱 전략은 대부분의 설계에서 절대 빠지지 않고 등장하는 것이다.
예전엔 백엔드/프론트에서의 캐싱만 생각을 했다면, 이번에는 메모리간의 데이터 이동, I/O간의 비용을 줄이기 위해 캐시를 사용한다는게 흥미로운 부분이였다.


4. 규칙에 따라 상태를 전이하는 패턴

  • 네트워크 (TCP 상태 전이): LISTEN → SYN_SENT → ESTABLISHED
  • OS (컨텍스트 스위칭): 프로세스의 ready / running / waiting 상태 전이
  • 안드로이드 (라이프사이클): onCreate → onStart → onResume → ...
  • 그림판 (제스처 상태, 도구 라이프사이클): 핀치 줌 진입과 같은 복잡한 로직을 쉽게 관리하기 위해 상태를 명시적으로 정의했다.
  • 게임 분기 (스와이프 벽돌깨기): 공 발사 전, 첫 번째 공이 내려온 이후 등등 적절한 콜백을 설계하기 위해 상태를 만들어서 관리했다.

게임을 제작할때 안드로이드 라이프사이클처럼 상태를 구분해서 다루면 어떨까? 라고 생각을 해서 직접 설계도 하고 그리고 콜백함수도 넣었던 적이 있었다.

네트워크시간에서 상태전이 그래프를 보고 진짜 최종완성급의 설계를 보니 지금까지 부족했던 점도 떠올랐다.
예전에는 상태를 대충 머릿속으로 생각하고, 다음 상태로 가는 조건은 따로 그림으로 표현하지 않았는데, TCP연결과정에서 쓰인 다이어그램을 보고 그렇게 다이어그램으로 표현하면 복잡성이 더 낮아진다는 것을 알았다.
원래 다이어그램으로 그리면 가독성이 안좋아지기 마련인데, 이번기회에 가독성을 챙기며 그리는 법을 배웠던 것 같다.


5. 큐를 사용하는 패턴

  • OS (컨텍스트 스위칭, 스케줄링, 모니터 큐): ready 큐에서 작업을 기다리고, 스케줄링을 위해 레벨별 큐를 둔다.
  • 하늘인편 (편지 큐): 편지를 대기하기 위한 큐, 재시도를 위한 큐, 잘못된 정보를 모으기 위한 큐 등을 설계했다.
  • 그림판 (polling): GPU에서 CPU로 전송해야하는 데이터를 idle 시간 때 옮기기 위해 waiting 큐를 사용했다.

이건 너무 흔하지만, 너무 자주쓰여서 빼먹을 수가 없었다.
알고리즘 문제를 풀때 사용하는 것은 물론이고, 실제 개발할때는 뭔가를 대기한다는 개념이 꽤 많이 존재하고, 그런 파생되는 트릭들도 많이 사용된다.

OS에서 큐들을 보며 느낀것은, 예전에 하늘인편을 만들때 대기 큐를 두고, 그 대기큐를 소모하기 위해 cron을 사용했던 경험이 있다. 이렇게 cron을 사용해서 수동적으로 큐를 소모시키는 것 보다, 원소 안에 메타데이터를 두고, 그것을 보고 큐에 요소가 새로 추가됐을때 큐를 소모시키는 전략이 더 나았을 것이라는 개선방향이 떠올랐다.

이뿐만 아니라 나중에 OS에서 배운 설계방법을 가지고 다른설계에 적용할 생각을하니 설레는 것 같다.


6. 제한된 자원을 누구에게 줄지 결정하는 패턴

  • DB (버퍼 풀 교체): 버퍼 풀에서 내보낼 블록을 고른다.
  • 컴구 (캐시 교체): 캐시를 교체할 때 나오는 기법
  • OS (스케줄링, 페이지 교체): 누구에게 CPU 자원을 줄지, victim page 선정 기법

이것은 아까 얘기했던 페이지, 블록 등으로 나누는것과도 연관이 있다.
자주쓰지 않는 자원을 캐시에서 내려야하는데, 정해진 단위가 없다면 구현하기 매우 복잡하다는 것을 알기에 일정한 단위로 분할하는것이 좋고, 그렇게 단위가 생겼으면, 그때부터 공통된 정책을 사용할 수 있다.

이번에 신기했던 것은 컴구에서 배운 캐시도 그렇고, OS도 그렇고 정확한 LRU방식을 사용하기는 어려우니, LRU근사를 하는 방식을 배웠는데, 이런 비트단위 연산을 사용해서 근사를 하려는 아이디어가 많이 독창적이여서 신기했던 기억이 난다.


7. 바뀐 것만 업데이트하는 패턴

  • OS: 페이지를 교체할 때 변경된 페이지만 디스크에 쓴다.
  • DB (버퍼풀): 같은 블록이 여러 번 수정되어도 매번 디스크에 쓰지 않고, 한 번만 디스크에 반영한다.
  • 그림판: DirtyRect만 렌더링해서 렌더링 비용을 줄인다. 연산시에는 DirtyTile만 연산한다.

이것도 2번에서 얘기한 분할 기법과도 연관이 많다.
쓰다보니 그렇게 페이지, 블록, 타일 단위로 분할하는 것에 큰 감명을 느꼈나보다. 대부분 비슷한데 아무래도 그림판 설계를 했어서 공감되는 면이 많아서 그런가도 싶다.

이렇게 변경된 부분만 업데이트하는건 어떻게 보면 최적화를 위해서 당연한 방식일 수 있는데, 가장 중요한건, 분할된 조각단위로 dirty여부를 체크한다는 것이다.
그림판 만들때는 dirtyRect를 별도로 만들어서 다뤘는데, 타일단위로 수정여부를 체크하니까 더 편해지고 설계가 아름다워졌다는 느낌을 받았다. (너무 추상적이지만 나도 안다)


이렇게 자주 사용되는 패턴을 한번 정리해보니 결국 한정된 자원을 사용해야한다는 문제를 바탕으로 나온 개념들이라는 생각이 든다.
이런 철학들은 선배 개발자가 잘 닦아놓은 길이라고 생각하고 다른 문제를 풀 때 아이디어를 적극 빌려봐야겠다.

이번에는 과목을 듣기 전에는 생각하지 못했던 점을 적어보려고 한다.


새로 알게된 점

Index

예전의 나는 "인덱스만 걸면 B+트리가 알아서 잘 해주겠지"라는, SQLD 자격증만 딴 개발자의 관점으로 DB를 짰다.
물론 Prisma를 쓰다 보니 SQL이 이상하게 번역됐나 싶어 직접 쿼리 프로세싱을 뜯어보면서 "왜 이 인덱스를 안 타지? 예상보다 느린데?"라고 생각한 적은 있었다.

하지만 인덱스도 맨날 타는건 아니라는것을 알았다.
non-clustering index, clustering index가 있는지도 몰라서 무의식적으로 모든 인덱스가 clustering index인줄 알고 설계했었다.
따로 찾아보니 PostgreSQL은 기본적으로 dense index이고 (심지어 PK조차!), sparse index와 비슷한 BRIN index가 있고 (min, max를 저장), CLUSTER를 사용해서 특정 시점에 테이블을 한 번 재정렬하는 것도 가능하다는 것을 알았다.

이제는 DB를 고를때도 아무거나 고르지 않고 각각의 DB의 트레이드오프를 살펴보고 잘 선정해야겠다.
지금 하는 프로젝트 중에 데이터 레코드가 100만 개가 넘는 플젝이 있는데, 쓸모없는 인덱스는 제거하고 DB 튜닝으로 성능을 끌어올릴 수 있는 방법도 찾아봐야겠다.

SIMD

무거운 작업은 WASM을 사용해서 SIMD연산으로 구현하려고 했지만, 정작 SIMD가 정확히 어떻게 CPU단에서 병렬화를 한다는건지 잘 알지 못했던 것 같다.
그런데 5-stage pipeline의 EX 단계에서 여러 개의 연산 유닛으로 작업을 보낸다는 걸 배우면서, SIMD가 어떤 구조로 이뤄지는지 감을 잡았다.

Context Switching으로 비롯된 Race Condition

Java나 Kotlin에서는 코루틴 같은 방법을 따로 써야 하지만, JS는 그냥 async만 붙이면 비동기 함수다. JS에서는 race condition을 겪어본 적이 없어서, 공유 자원을 다룰 때 생기는 문제 자체를 인지하지 못하고 있었다.
비동기 함수 여러 개를 동시에 쓸 때의 공유 자원 문제는 알았지만, CPU instruction과 context switching 과정에서 생기는 race condition은 느껴본 적도, 존재를 알지도 못했다. 읽기와 쓰기 사이에 context switching이 끼어들 수 있다는 것조차 몰랐던 거다.

그래서 웹워커를 쓸 때 Atomics라는 게 있고, SharedMemory에 읽고 쓸 때는 Atomic을 써야 한다는 걸 알게 된적은 있지만 이때도 그걸 왜 써야하는지는 정확히 몰랐다.

Java나 Kotlin 사용자가 실제로 이런 race condition을 직접 다루는지도 찾아봤는데, synchronized로 개발자가 모니터를 직접 만들고 관리한다는 걸 몰랐다.
Java에서는 뮤텍스 락, 트라이 락, 모니터, bounded buffer 문제를 직접 다룬다는 걸 알고 나니, 나중에 CS를 더 깊게 공부하고 싶으면 그런 언어를 해보는 것도 좋겠다는 생각이 들었다.

단적인 예로 코루틴은 suspend를 쓰기 때문에 mutex lock으로 suspend시키는 방식이 가능하다. 그러면 스레드를 많이 만들지 않아도 되고, 스레드가 적으니 모니터처럼 스레드를 막는 작업도 덜 쓰게 된다고 한다. 이벤트루프와 멀티쓰레딩을 같이 사용하는 전략이 있다는 것을 보고 신기했다.

데드락도 마찬가지다. JS는 context switching에서 비롯된 race condition이 없으니 이런 걸 다룰 일조차 없었고, 그래서 데드락 발생 가능성을 아예 배제하고 지냈다. 다른 언어를 쓰면 이렇게 데드락을 막는 설계도 기본기에 포함된다는 걸 느꼈다.
예전에 GC 타이밍 때문에 애먹었을 때도 Java는 GC를 제어할 수 있다는 걸 보고 언어 간 차이를 느꼈는데, 동기화에서 또 한 번 실감했다. 컴퓨터를 더 잘 알려면 JS가 아닌 다른 언어도 익힐 필요가 있겠다.

예전에 Redis가 싱글 스레드로 동작한다는 글을 본 적이 있다. 그땐 semaphore가 뭔지 몰라서 이해를 못 했는데, 배우고 나니 멀티 스레드를 포기한 대신 캐시 지역성, CPU instruction, context switch 비용처럼 공유 자원을 허용하면서 생기는 오버헤드를 모두 제거하는, 꽤 합리적인 트레이드오프가 숨어 있다는 걸 알게 됐다. 교수님이 데드락 해결법으로 "자원을 공유하지 말자"는 얘기를 한 적이 있었는데, 공유 자원을 만들지 말지 여부 자체도 선택이라는 것을 알게됐다.


마무리하며

원래는 "이런 구조를 쓰면 좋겠다"라고만 생각했는데, 선대 개발자들이 미리 경험했던 문제와, 이미 닦아놓은 방법들을 보면서 배움과 인사이트를 얻는 수업이었다.

다음에 공부할때는 지금 배운 cs지식과 연계해서 더 깊게 찾아볼 수 있을 것 같고, 다른 기본적인 cs지식도 공부해야겠다는 생각이 들었다. (그래서 다음달부터 JS 스터디 하기로 함.)

profile
대학생 개발자

1개의 댓글

comment-user-thumbnail
2026년 6월 29일

안녕하세요.
제 이름은 매그니타 티모시입니다.
방금 당신의 프로필을 봤어요.
당신과 친구가 되고 싶어요.
많은 이야기를 나누고 싶은데,
어떻게 생각하세요?
(magnitatimothy@gmail.com)으로 답장 주세요.

답글 달기