Blocking & Non-Blocking I/O
async : 이벤트 핸들러 (callback)에 의해 처리 (callback 함수가 호출되기까지 다른 작업 가능)
sync : 이벤트를 자신이 직접 처리(확인의 주체가 유저 프로세스이며, 다 될때까지 기다리거나 스스로 확인)
block : 완료까지 대기(리턴되기 전까지 멈춤)
non-block : 미완료라도 즉시 리턴
간단히 까페에서 커피를 주문하는 것을 예로 들어보면,
1. 커피를 타달라는 요청이 왔다.
2-1. 이 때 커피가 있으면 타준다(블로킹/넌블로킹 모두)
2-2. 커피가 없는 경우 블로킹 : '잠깐만요'하고 사러 간다. / 넌블로킹 : 커피가 없다고 말하고 사러 간다.
3-1. 동기 : 커피가 타졌는지 안타졌는지 내가 확인한다.
3-2. 비동기 : 벨이 울리면 받으러 간다.
즉, 이범주로 보면 select는 sync + non-block으로 보는 것이 맞다고 합니다.(async + block 사례는 뭐가 있을까요..)
추가적으로, signal-driven I/O도 async + non-block 으로 보는 것이 맞다고 합니다.
(일반적인 async는 non-block인걸로..)

보다 자세한 내용은 SLiPP, TCP/IP Review & I/O Model 을 참조하시는게 좋을거 같네요.
유저단계의 프로세스는 I/O 작업을 직접 수행할 수 없으며, 커널에게 요청해야 한다.
운영체제가 제공하는 서비스를 호출하는 함수를 시스템 호출(system call)이라고 한다.
Blocking I/O과 Non-Blocking I/O
Blocking : 유저 프로세스가 시스템 호출을 하고나서 결과가 반환되기까지 다음 처리로 넘어가지 않음 Non-Blocking : 호출한 직후에 프로그램으로 제어가 돌아와서 시스템 호출의 종료를 기다리지 않고 다음 처리로 넘어갈 수 있음
Synchronous I/O와 Asynchronous I/O
동기 : 작업을 요청한 후 작업의 결과가 나올 때까지 기다린 후 처리 (프로세스는 커널에 지속적으로 I/O 준비사항을 체크) 비동기 : 직전 시스템 호출의 종료가 발생하면 그에 따른 처리를 진행

Synchronous blocking I/O

가장 기본적인 I/O 모델로, 파일을 읽고 쓰는 일반적인 read, write와 같은 I/O 시스템 호출을 의미한다. (특별한 설정이 없으면 blocking으로 동작한다.)
유저 프로세스가 read()을 호출한 후 커널은 데이터가 사용자 버퍼에 복사되기 전까지 리턴하지 않으며, 유저 프로세스는 자신의 작업을 중단한 채 대기한다. (CPU 등의 자원 낭비)
따라서, 여러 클라이언트가 접속하는 서버를 Blocking방식으로 구현할 경우 하나의 클라이언트가 I/O 작업을 진행하면 해당 프로세스(또는 쓰레드)가 진행하는 작업을 중단하게 되므로, 클라이언트별로 프로세스(또는 쓰레드)를 만들어 연결시켜야 한다. 하지만 접속자 수가 증가하면 프로세스(또는 쓰레드)가 증가한다.
Synchronous non-blocking I/O

커널은 data를 읽은 후 버퍼에 저장하고 그 내용을 유저에게 복사해준다. 버퍼는 커널이 가지고 있는 메모리에 적재되므로 메모리간 복사가 일어나 I/O보다 빠른 속도로 받아올 수 있다. 그러나 버퍼가 비어있다면 커널은 즉시 에러(EWOULDBLOCK)를 반환하여, 유저 프로세스가 지속적으로 read()을 호출하여 I/O 준비사항을 체크하도록 한다. (polling 방식) 이에 반복적으로 시스템 호출이 일어나므로 cpu 등의 자원이 낭비된다.
Asynchronous blocking I/O ( 이부분이 오류가 있다고 하네요. )

select나 poll과 같은 시스템 호출을 이용하여 I/O 다중화를 하기 위한 목적으로 사용된다.
(select와 poll은 여러개의 descriptor에서 데이터가 준비되었는지 검사를 수행하여 준비가 된 descriptor가 발견될 경우 리턴하여 여러 개의 I/O를 처리할 수 있다. 다만, select는 관리 file descriptor 수에 제한이 있고, poll은 제한은 없으나 file descriptor 당 체크 마스크의 크기가 커서 접속자 수가 늘어나면 성능이 떨어진다.)
Signal-driven I/O

미리 sigaction() 시스템 호출을 하여 시그널 핸들러를 미리 등록한 후 커널에 데이터가 준비되었을 경우 SIGIO 라는 시그널을 발생시켜 데이터의 도착을 알려주는 모델이다.
이 모델은 데이터가 준비되기 전까지 어플리케이션은 block되지 않으므로 유저 프로세스는 다른 작업을 처리하거나 대기하면 된다.
Asynchronous non-blocking

aio_read() 시스템 호출을 하면 file descriptor와 buffer의 포인터, buffer의 크기 및 file offset, 완료시 통지 방법 등을 커널에 전달하고, 커널은 즉시 리턴한다. 이 후 데이터가 사용자 버퍼에 복사되어 I/O 준비가 완료되면 시그널을 발생하여 완료를 알려준다.

Prim Algorithm
초기화 과정에서 한 개의 vertex 로 이루어진 초기 그래프 A 를 구성한다. 그리고나서 그래프 A 내부에 있는 vertex 로부터 외부에 있는 vertex 사이의 edge 를 연결하는데 그 중 가장 작은 weight 의 edge 를 통해 연결되는 vertex 를 추가한다. 어떤 vertex 건 간에 상관없이 edge 의 weight 를 기준으로 연결하는 것이다. 이렇게 연결된 vertex 는 그래프 A 에 포함된다. 위 과정을 반복하고 모든 vertex 들이 연결되면 종료한다.
Time Complexity
=> 전체 시간 복잡도 : O(E log V)
Map
자바에서 제공하는 HashMap과 Hashtable은 Map 인터페이스를 상속받아 구현되어 데이터를 키와 값으로 관리하는 자료구조이다.
큰 특징으로는 키(Key)가 데이터를 추출할 때 구분자로 활용하는 방식을 취하는데 이는 리스트 인터페이스와 같은 자료구조보다 탐색에 있어 더 높은 효율을 기대할 수 있다.
HashMap과 Hashtable의 차이점은 동기화와 반환값이다.
동기화
HashMap의 경우 동기화를 지원하지 않는다.
반면 다중 스레드 환경에서 Hashtable은 동기화를 지원하기 때문에 실행 환경에 따라 구분하여 사용하면 된다.
하지만 한 자바 관련 서적에 의하면 Vector의 상위호환(?)개념인 ArrayList의 사용을 권장하듯 새로운 버전인 HashMap을 활용하고 동기화가 필요한 시점에서는 Java 5부터 제공하는 ConcurrentHashMap을 사용하는 것이 더 좋은 방법이라 표현한다.
추가로 속도적인 측면에서도 구형이라 할 수 있는 Hashtable은 동기화 처리라는 비용때문에 HashMap에 비해 더 느리다고 한다.
반환값
HashMap은 저장된 요소들의 순회를 위해 Fail-Fast Iterator를 반환한다.(ConcurrentHashMap의 경우에는 Fail-Safe Iterator)
Hashtable은 같은 경우 Enumeration을 반환한다.
여기서 Enumeration과 Iterator는 컬렉션에 저장된 요소를 접근하는데 사용되는 인터페이스이다.
Enumeration은 컬렉션 프레임워크 이전에 사용되던 인터페이스로 Iterator의 사용을 권장한다.
Iterator엔 remove() 메소드가 추가되었고 메소드 네이밍이 간략화되었다.
그리고 다른 스레드(lock된 상황에서)에서 해당 자료에 요소를 수정(삽입, 삭제, 수정 등)이 발생하면 ConcurrentModificationException을 발생시켜 일관성을 보장한다.
이를 Fail-Fast Iterator라 한다.
ConcurrentHashMap의 경우 Map의 복사본을 참조하는 Iterator를 반환하며 다시 반환받은 시점에 Map에 수정이 있을 경우 해당 Iterator는 반영되지 않는다. 고로 ConcurrentModificationException또한 발생하지 않는다. 이는 약한 일관성(Weakly Consistent)를 제공하지만 다중 스레드상황에서 해당 Map의 무결성(?)을 보장한다.
추가로 ListIterator가 있는데 이는 단방향만을 제공하는 Iterator의 기능을 향상시킨 것이다.
Iterator it = list.iterator(); it.next();
와 같이 사용된다면
ListIterator li = list.listIterator(); li.next(); li.previous();
와 같이 활용할 수 있다.
다만 List 인터페이스를 상속한 컬렉션에서만 사용가능하다.
Anomaly
정규화를 해야 하는 이유는 잘못된 테이블 설계로 인해 Anomaly(이상 현상)가 나타나기 때문이다.
여기서 Anomaly가 무엇인지 알아보자.
Ex)
{Student ID, CourseID, Department, Course ID, Grade}
1) 삽입 이상(Insertion Anomaly)
기본키가 {Student ID, Course ID} 인 경우
Course를 수강하지 않은 학생은 Course ID가 없는 현상이 발생한다. 결국, Course ID를 Null로 할 수 밖에 없는데, 기본키는 Null이 될 수 없으므로 테이블에 추가될 수 없다.
굳이 삽입하기 위해서는 '미수강'과 같은 Course ID를 만들어야 한다.
불필요한 데이터를 추가해야 삽입할 수 있는 상황 => Insertion Anomaly
2) 갱신 이상(Update Anomaly)
만약 어떤 학생의 전공 {Department}이 "컴퓨터 -> 음악"으로 바뀌는 경우
모든 Department를 "음악"으로 바꾸어야 한다. 그러나 일부를 깜빡하고 바꾸지 못하는 경우, 제대로 파악하지 못한다.
일부만 변경하여 데이터가 불일치하는 모순의 문제 => Update Anomaly
3) 삭제 이상(Deletion Anomaly)
만약 어떤 학생이 수강을 철회하는 경우, {Student ID, Course ID, Department, Course ID, Grade}의 정보 중 Course ID를 삭제하면 Student ID, Department와 같은 학생에 대한 정보도 함께 삭제된다.
튜플 삭제로 인해 꼭 필요한 데이터까지 함께 삭제되는 문제 => Deletion Anomaly
객체지향 프로그래밍
객체 지향 프로그래밍은 OOP(Object Oriented Programming)이라고도 한다.
프로그래밍에서 필요한 데이터를 추상화시켜 상태와 행위를 가진 객체를 만들고 그 객체들 간의 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍 방법이다.
[장점]
- 코드의 재사용성이 높다.
- 누군가가 만든 클래스를 가져와 사용할 수 있고 상속을 통해 확장할 수도 있다.
- 유지보수가 쉽다.
- 수정해야 할 부분이 클래스 내부에 멤버 변수 혹은 메소드로 존재하기 때문에 해당 부분만 수정하면 된다.
- 대형 프로젝트에 적합하다.
- 클래스 단위로 모듈화시켜서 개발할 수 있으므로 업무 분담하기가 쉽다.
[단점]
- 처리 속도가 상대적으로 느리다.
- 객체가 많으면 용량이 커질 수 있다.
- 설계 시 많은 노력과 시간이 필요하다.
객체 지향 프로그래밍의 특징
- 추상화
불필요한 정보는 숨기고 필요한 정보만을 표현함으로써 공통의 속성이나 기능을 묶어 이름을 붙이는 것이다.
- 캡슐화
속성과 기능을 정의하는 멤버 변수와 메소드를 클래스라는 캡슐에 넣는 것이다. 즉, 관련된 기능(메소드)과 속성(변수)을 한 곳에 모으고 분류하기 때문에 재활용이 원활하다.
목적 : 코드를 수정 없이 재활용하는 것
또한, 캡슐화를 통해 정보 은닉이 가능하다.
- 상속
부모 클래스의 속성과 기능을 그대로 이어 받아 사용할 수 있게 하고 기능의 일부분을 변경해야 할 경우, 상속 받은 자식 클래스에서 해당 기능만 다시 수정(정의)하여 사용할 수 있게 하는 것이다.
상속을 통해서 클래스를 작성하면 보다 적은 양의코드로 새로운 클래스를 작성할 수 있다.
또한, 코드를 공통적으로 관리하여 코드 추가 및 변경이 용이하다.
- 다형성
하나의 변수명, 함수명 등이 상황에 따라서 다른 의미로 해석될 수 있는 것이다.
즉, 오버라이딩, 오버로딩이 가능하다.
객체 지향 설계 원칙
이와 관련된 내용은 심도 깊으나, 여기서는 간단하게 무엇을 의미하는지만 정리한다. 추후 추가 예정.
- SRP(Single Responsibility Principle) : 단일 책임 원칙
클래스는 단 하나의 책임을 가져야 하며, 클래스를 변경하는 이유는 단 하나의 이유여야 한다.
- OCP(Open Closed Principle) : 개방 폐쇄 원칙
확장에는 열려있고, 변경에는 닫혀있어야 한다.
- LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.
- ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.
- DIP(Dependency Inversion Principle) : 의존 역전 원칙
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.