Front-end Developer

0%

문서 영역을 편집가능하게 만들기

contentEditable은 키워드가 빈 문자열이고, ture 또는 false를 가지는 열거형 속성(enumerated attribute)이다. 빈 문자열과 true 키워드는 true state에 매핑된다. false 키워드는 false state에 매핑된다. 그리고 inherit state인 세 번째 state가 있는데 missing value 또는 invalid value를 default로 가지는 값이다. true state는 element가 편집 가능함을 의미한다. inherit state는 element의 부모가 편집이 가능 여부를 상속받음을 의미한다. false state는 element가 편집불가함을 의미한다.

예를 들어 유저가 HTML을 이용해서 아티클을 쓸 수 있는 새로운 아티클을 발행하기 위한 form과 textarea가 있는 페이지를 생각해보자.

1
2
3
4
5
6
7
<form method="POST">
<fieldset>
<legend>New article</legend>
<textarea name="article">&lt;p>Hello world.&lt;/p></textarea>
</fieldset>
<p><button>Publish</button></p>
</form>

scripting가 활성화되면, textarea element에 contenteditable 속성을 사용하면 서식이 있는 텍스트 컨트롤 요소로 바꿀 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<form method="POST">
<fieldset>
<legend>New article</legend>
<textarea id="textarea" name="article">&lt;p>Hello world.&lt;/p></textarea>
<div id="div" style="white-space: pre-wrap" hidden><p>Hello world.</p></div>
<script>
let textarea = document.getElementById('textarea');
let div = document.getElementById('div');
textarea.hidden = true;
div.hidden = false;
div.contentEditable = 'true';
div.oninput = (e) => {
textarea.value = div.innerHTML;
};
</script>
</fieldset>
<p><button>Publish</button></p>
</form>

다음과 같이 풍부한 효과를 내기 위해 사용할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<title>Live CSS editing!</title>
<style style="white-space:pre" contenteditable>
html {
margin: 0.2em;
font-size: 2em;
color: lime;
background: purple;
}
head,
title,
style {
display: block;
}
body {
display: none;
}
</style>
</html>

References
Making document regions editable: The contenteditable content attribute

HTML에서 <input>, <textarea>, <select>와 같은 form 엘리먼트들은 자체적으로 내부 상태와 user의 입력값에 따른 내부 상태를 가진다. 그런데 리액트에서 변경가능한 state는 컴포넌트의 state로 관리되고, setState()를 통해서 업데이트하는 방식을 취한다.

1
2
3
4
5
6
7
<form>
<label>
Name:
<input type='text' name='name' />
</label>
<input type='submit' value='Submit' />
</form>

위의 코드는 name의 입력을 받아서 사용자가 폼을 제출하면 새로운 페이지로 이동하는 기본 HTML 동작을 수행한다. 리액트에서도 동일한 엘리먼트가 제공되기 때문에 동일한 방법의 JSX로 작성해서 사용하면 된다. 다만 폼의 제출을 처리하고 사용자가 폼에 입력한 데이터에 접근할 수 있는 자바스크립트 기능(함수)이 있으면 편리하다. 이를 위한 표준 방식이 제어 컴포넌트이다. 리액트에서 폼을 처리하는 방식은 제어 컴포넌트비제어 컴포넌트 두 가지 방식이 있다.


제어 컴포넌트

폼 값을 DOM이 아니라 리액트로 관리하는 방식이다. 이 방법을 사용하면 ref와 같은 참조를 사용할 필요가 없고, 명령형 코드를 사용할 필요도 없다.즉 리액트가 폼의 상태를 모두 제어하는 것이다.이러한 제어 컴포넌트가 표준 방식이라 일컬어지는 이유는 리액트 어플리케이션에서 발생하는 어떤 데이터의 변화도 single soure of truth를 지향하기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { useState } from 'react';

const NameForm = () => {
const [state, setState] = useState('');

const handleChange = (e) => {
setState(e.target.value);
};

const handleSubmit = (e) => {
alert('A name was submitted: ' + state);
e.preventDefault();
};

return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type='text' value={state} onChange={handleChange} />
</label>
<input type='submit' value='Submit' />
</form>
);
};

export default NameForm;

위의 예제는 폼이 제출될 때 이름을 기록하는 코드이다. 위와 같이 사용자의 입력을 리액트의 state를 활용해서 관리하고 setState()를 통해 업데이트하는 방식을 통해서 single soure of truth로 만들게 되면 폼을 렌더하는 리액트 컴포넌트는 폼에 발생하는 사용자 입력값을 제어할 수 있게 된다. 즉 name의 value 어트리뷰트는 form 엘리먼트에 의해 설정되기 때문에 보여지는 value는 항상 useState로 설정한 state가 되고, 이 state를 업데이트 하기 위해 사용자가 입력하는 모든 입력에서 handleChange가 동작하기 때문에 사용자가 입력할 때 보여지는 value가 업데이트 된다. 이렇게 input의 값은 항상 리액트의 state에 의해 결정되고, 리액트에 의해서 값이 제어되는 폼 엘리먼트를 제어 컴포넌트라고 한다. 다만 state가 변경될 때마다 리렌더링이 발생되는 리액트의 특성상 제어가 제어 컴포넌트는 여러번 재 렌더링 된다.

비제어 컴포넌트

제어 컴포넌트에서 폼 데이터가 리액트 컴포넌트에서 제어된 것과 달리 DOM 자체에서 폼 데이터가 이루어지는 방식이다. 비제어 방식으로 컴포넌트를 작성하려면 모든 state의 업데이트에 대해 이벤트 핸들러를 작성하는 대신 ref를 사용해서 직접 DOM에 접근할 수 있다. ref는 참조라고 불리는데 리액트에서 컴포넌트의 생명주기 값을 저장하는 객체이다. 이 참조는 useRef라는 훅을 통해 사용가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useRef } from 'react';

export default function AddColorForm({ onNewColor = (f) => f }) {
const txtTitle = useRef();
const hexColor = useRef();

const submit = (e) => {
e.preventDefault();
const title = txtTitle.current.value;
const color = hexColor.current.value;
onNewColor(title, color);
txtTitle.current.value = '';
hexColor.current.value = '';
};

return (
<form onSubmit={submit}>
<input ref={txtTitle} type='text' placeholder='color title...' required />
<input ref={hexColor} type='color' required />
<button>ADD</button>
</form>
);
}

위의 코드는 사용자로부터 색의 title 값과 색상값을 입력받아서 ADD 버튼 클릭시 폼이 제출되도록 하는 코드이다. 여기서 txtTitle과 hexColor에 각각 useRef를 통한 참조를 만들어주었다. 이렇게 참조를 걸어주게 되면 참조의 값을 직접 JSX에서 설정할 수 있게 된다. 이를 통해 DOM 엘리먼트를 직접 참조하는 참조 객체에 대한 current 필드를 생성하고, 이 필드를 통해서 DOM엘리먼트에 접근하여 엘리먼트의 값을 얻을 수 있다. 그리고 ADD 버튼을 사용자가 클릭하면 submit 함수를 호출한다.

submit이 하는 일은 먼저 e.preventDefault()를 통해 폼 요소가 기본적으로 가지고 있는 submit 동작에서 서버에 폼을 보내고자 하는 동작을 막는 것이다. 그 다음으로는 ref의 참조를 통해 폼 엘리먼트의 현재 값을 얻어온다. 그리고 이를 onNewColor()를 통해 부모에게 전달한다. 그리고 초기화를 위해 두 입력 값 txtTitle, hexColor에 대한 value를 ''로 설정한다. 이는 DOM 노드의 값을 직접 변경한 것이다. 이렇게 작성하게 되면 DOM을 통해 폼 값을 저장한 것이므로 명령형 코드를 작성하였고, 제어되지 않는 컴포넌트를 작성한 것이다. 이런 비제어 컴포넌트는 리액트 외부에서 폼에 접근하여 입력 값을 처리하고 싶은 경우에 사용될 수 있다.


제어 컴포넌트 vs 비제어 컴포넌트

앞선 예시에서 리액트에서 폼을 다룰 때 제어 컴포넌트와 비제어 컴포넌트의 두 가지 방식으로 처리할 수 있다고 하였다. 제어 컴포넌트는 리액트에 의해서 사용자가 입력한 값이 제어되는 경우를 말한다. 비제어 컴포넌트는 form에 입력한 값이 리액트에서 의해서 작동하는 것이 아닌 리액트 외부에서 작동하는 것처럼 작동한다. 즉 사용자가 입력한 값이 리엑트의 state를 통해 state를 유지하면서 업데이트되는 방식과 같은 별도의 처리가 없어도 엘리먼트에 반영되는 것이다. 일반적으로 제어 컴포넌트를 사용하는 것이 표준방식이라 하는데 그렇다고 비제어 컴포넌트가 나쁘다거나 사용하면 안된다는 것은 아니다. 상황에 따라서 적절한 형테로 컴포넌트를 작성할 수 있다. 어떤 경우에 제어 컴포넌트 또는 비제어 컴포넌트를 써야하는지 정리해보면 다음과 같다.

비제어 컴포넌트

필요할 때 필드에서 값을 가져와야 한다. (pull the value from field)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Form extends Component {
handleSubmitClick = () => {
const name = this._name.value;
// do something with `name`
};

render() {
return (
<div>
<input type='text' ref={(input) => (this._name = input)} />
<button onClick={this.handleSubmitClick}>Sign up</button>
</div>
);
}
}

입력한 값을 제출할 때 ref를 통해 입력한 값을 가져올 수 있다. 위 코드에서는 값을 제출할 때 onClick 핸들러에서 입력한 값이 무엇인지를 얻을 수 있었다. 이는 기존 HTML에서 폼을 제출하는 방식과 유사하고, 가장 간단하게 폼을 구성하는 방식이다.

제어 컴포넌트

값을 밀어넣어 컴포넌트가 전달받은 값으로 변경된다. (kind of ‘pushes’ the value changes to the form component.)

제어된 방식에서 입력값은 prop과 이 값을 변경하기 위한 콜백을 받는다. 앞선 예제보다 보다 React적인 방법이라고 말할 수 있다. 입력한 값은 어떠한 방식으로 작성되던 반드시 state로 어딘가에 있어야 한다. state는 다른 컴포넌트의 state나 Redux와 같이 별도의 store에 저장되어 있을 수 있는데, 일반적으로는 아래와 같이 리액트의 state에 그 값을 저장한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Form extends Component {
constructor() {
super();
this.state = {
name: '',
};
}

handleNameChange = (event) => {
this.setState({ name: event.target.value });
};

render() {
return (
<div>
<input
type='text'
value={this.state.name}
onChange={this.handleNameChange}
/>
</div>
);
}
}

위 코드에서 새로운 문자를 입력할 때마다 handleNameChange가 호출된다. 이 함수는 input에 입력된 새로운 값을 가져와서 state의 값으로 set한다. 초기값이 ‘’이기 때문에 빈 문자열로 시작하지만 만약 a를 입력하면 handleNameChange를 호출하고, setState를 호출해서 입력된 값을 전달한다. 그러면 이 input은 a로 값이 바뀌었으므로 리렌더링된다. 이 상태에서 b를 입력하게 되면 input에 입력된 ab라는 값을 얻고, state의 값을 ab로 set한다. 그리고 input은 리렌더링 되고 이제 값은 a가 아닌 ab가 된다. 즉 form 컴포넌트는 명시적으로 값을 요청할 필요없이 항상 input의 현재값을 가지게 된다.

이 말의 의미는 데이터(state)와 UI(input)이 항상 동기화된다는 의미이다. state는 input에게 값을 제공하고, input은 form에 현재 값은 변경하도록 요청한다. 이 말은 즉 form 컴포넌트가 input의 변경사항에 대해 즉시 응답할 수 있다는 것이다. 예를 들어 validation과 같은 즉각 피드백, 모든 필드에 유효한 데이터가 없으면 버튼 비활성화, 신용 카드 번호와 같은 특정 입력 시행과 같은 상황이다.

여러가지 상황을 고려해서 제어 컴포넌트를 사용할지, 비제어 컴포넌트를 사용할지에 대해 결정할 수 있다. 만약 UI 피드백이 단순하다면 비제어 컴포넌트 방식이 더 괜찮을 수 있다. 그리고, 비제어 컴포넌트 방식을 사용했더라도 언제든지 제어 컴포넌트 방식으로 마이그레이션 할 수 있는 것이다.

특징 비제어 제어
일회성 값 검색(e.g. submit) O O
제출 시 validation O O
즉각적인 field validation X O
조건에 따른 제출 버튼 비활성화 X O
입력 형식의 강제 X O
하나의 데이터에 대한 여러 입력 X O
동적 입력 X O

References

uncontrolled components
Controlled Components
Controlled vs. Uncontrolled Components
Controlled and uncontrolled form inputs in React don’t have to be complicated
러닝 리액트 2판

02. 리액트를 위한 자바스크립트

변수 선언하기

  • ES2015이전: var를 사용한 변수 선언
  • ES2015이후(ES6): let, const를 키워드 추가

const 키워드

값을 변경할 수 없는 상수값 선언에 사용

값을 변경하려고 시도하면 콘솔창에서 에러가 발생한다.

1
2
3
4
5
6
7
8
9
//var: var로 선언된 변수는 값의 변경 가능
var pizza = true;
pizza = false;
console.log(pizza);

//const: 값의 변경 불가, 에러 발생

const pizza = true;
pizza = false;

let 키워드

구문적인 변수 영역 규칙(lexical variable scoping)의 지원

자바스크립트는 중괄호({})를 사용해서 코드 블록을 만드는데, 함수는 코드 블록이 별도의 변수 영역을 이루지만 if/else나 for 문에서는 그렇지 않다.

1
2
3
4
5
6
7
8
var topic = 'JavaScript';

if (topic) {
var topic = 'React';
console.log('block', topic); //block React
}

console.log('global', topic); //global React

위의 예제에서 if 블록 안의 topic 변수를 변경하면 if 밖에 있는 topic 변수도 같이 변경된다. 하지만 let을 사용하면 변수의 영역은 선언된 코드 블록 안으로 한정된다. 그러므로 아래 예제와 같이 var로 선언된 ‘JavaScript’변수는 var로 선언했을 때와 달리 변경되지 않은 채 유지된다. (코드 블록내에 let으로 선언된 변수에 한해서만 변경된다.)

1
2
3
4
5
6
7
8
var topic = 'JavaScript';

if (topic) {
let topic = 'React';
console.log('block', topic); //block React
}

console.log('global', topic); //global javaScript

for구문의 경우도 마찬가지인데, 아래 예제는 컨테이너 안에 5개의 div를 만드는 예제이다. 여기서 i를 for 루프 안에서 선언해도 var로 선언했기 때문에 글로벌에 i 변수가 생기고, i가 5가 될 때까지 for 루프를 돈다. 그런데 i의 값이 글로벌 변수 i인 5이기 때문에 어떤 div 박스를 클릭해도 인덱스는 5로 표시된다.

1
2
3
4
5
6
7
8
9
10
11
12
var div,
container = document.getElementById('container');

for (var i = 0; i < 5; i++) {
div = document.createElement('div');
div.onclick = function () {
alert('This is box #' + i);
};
container.appendChild(div);
console.log(i);
}
console.log('global', i); //5

하지만 let으로 선언하면 i의 영역이 for문의 코드 블록 안으로 제한되기 때문에 각 박스를 클릭하면 동일하게 5가 표시되는 것이 아니라 해당하는 박스의 인덱스가 표시된다. 즉 let으로 i의 영역을 제한하는 것이다.


References
러닝 리액트 2판

makeVar

const cartItems = makeVar([]);

선택적 초기 값으로 reactive 변수를 생성합니다.

변수의 값을 읽거나 수정하는 데 사용하는 reactive 변수 함수를 반환합니다.
사용 방법은 아래와 같습니다.

Reactive variables

Apollo Client 반응성(reactivity) 모델에 통합된 상태 컨테이너(state containers)

Apollo Client3의 새로운 기능인 reactive variables는 Apollo Client 캐시 외부의 로컬 상태를 나타내는 유용한 매커니즘입니다. 캐시와 분리되어 있기 때문에 reactive variables는 어떠한 타입과 구조의 데이터도 저장할 수 있고, GraphQL구문을 사용하지 않고도 어플리케이션의 어느 곳에서나 상호 작용할 수 있습니다. 가장 중요한 것은 reactive variables를 수정하면 이 변수에 의존하는 모든 활성 쿼리(query)의 업데이트가 트리거된다는 것입니다. 추가로 이 업데이트는 useReactiveVar를 쓰는 컴포넌트의 react 상태(State)를 업데이트 합니다.

만약 어떤 query의 요청된 필드가 변수의 값을 읽는 read 함수를 정의하는 경우에는 query가 reactive variables에 의존합니다.

Creating

makeVar를 이용해서 reactive variables를 만듭니다.

1
2
3
import { makeVar } from '@apollo/client';

const cartItemsVar = makeVar([]);

이 코드는 초기 값으로 빈 배열을 사용하여 reactive variables를 만듭니다.

Reading

reactive variables의 값을 읽으려면 makeVar를 인수 없이 호출합니다.

1
2
3
4
const cartItemsVar = makeVar([]);

// Output: []
console.log(cartItemsVar());

Modifying

reactive variables의 값을 수정하려면 makeVar와 하나의 인수(변수의 새로운 값)에 의해 반환된 함수를 호출합니다.

1
2
3
4
5
6
7
8
9
10
11
const cartItemsVar = makeVar([]);

cartItemsVar([100, 101, 102]);

// Output: [100, 101, 102]
console.log(cartItemsVar());

cartItemsVar([456]);

// Output: [456]
console.log(cartItemsVar());

Reacting

Reacting이라는 이름처럼 reactive variables는 어플리케이션에서의 reactive한 변화를 트리거할 수 있습니다. reactive variable의 값을 수정하려고 하면 쿼리는 해당 변수에 종속되어 있기 때문에 새로고침되고, 어플리케이션의 UI가 즉각 업데이트됩니다. useReactiveVar hook을 사용하면 query를 wrapping하지 않고 리액트 컴포넌트를 state에 바로 포함시킬 수 있습니다.


References
reactive-variables
makeVar

상태 관리

상태 관리가 되려면 아래 기능을 할 수 있어야 한다.

  • 최초값 저장
  • 현재 값 읽기
  • 값을 업데이트

Recoil은 왜 필요할까?

  • React의 내장 상태관리 기능의 한계점 극복
  • 최대한 react스러운 API 유지
  • 사용하기 위한 부속 라이브러리 최소화

React 상태 관리 로직의 한계점

  • 컴포넌트의 상태는 공통되는 부모 컴포넌트까지 올라가야 하고, 심할 경우 어플리케이션 상단까지 올라아가햔다.
  • Context API는 확정되지 않은 수의 값을 저장하는데 적합하지 않고, 최적화 관점에서 한계점이 명확하다. state관리보다는 의존성 주입의 개념에 가깝다고 볼 수 있다.

Recoil의 접근 방법

  • React Tree에 직교되는 형태로 존재하는 방향 그래프로 구성되어 있다. 예를 들어 Redux를 쓰려면 react-redux를 사용해서 react와 연결시켜주어야 했으나 Recoil은 그런 장치가 필요없다.
  • 상태의 변경은 이 그래프를 따라 React component로 흘러들어간다. 따라서 component의 로직을 건드리지 않아도 상태 데이터를 단독으로 변경할 수 있다.

Recoil의 철학

  • 보일러 플레이트가 적다.
  • React의 로컬 상태와 유사한 간단한 인터페이스
  • Concurrent Mode와 호환
  • 코드 상호간의 낮은 결합도를 통해 Code spliting 용이성 확보
  • 파생 데이터를 사용함으로써 데이터를 사용하는 컴포넌트에서 임의로 데이터를 바꾸는 로직을 가져가지 않아도 된다. 가져와서 useEffect로 바꿔주기를 하지 않고, 로직 자체를 Recoil atom에 귀속시킬 수 있다.

Core concept

유연하게 상태 관리 가능

공통적으로 필요한 데이터를 어떻게 저장할 것인가?

  • ContextAPI를 사용하면 다이나믹하게 구성할 수 없고, Coupling이 발생한다.
  • Provider가 추가될 떄마다 Tree는 다시 reconciling을 해줘야 하는 이슈
  • React의 로컬 컴포넌트 state와 동일하게 batching와 같은 작업들이 모두 라이브러리 내부에서 처리된다.

파생 데이터 생성이 용이

  • 상태와 관련 있거나 상태로부터 만들어진 것들
  • 상호 의존적인 state를 만들 필요가 없다. (두 개 이상의 atom을 참고한 또 하나의 atom..)
  • pure function으로 atom 데이터를 사용할 수 있도록 해준다. 정말 데이터에 변화가 있을 때만 recompute 하도록 해준다.

어플리케이션 단의 상태 observing 가능


설치

1
2
3
4
5
npm install recoil

or

yarn add recoil

ESLint

eslint-plugin-react-hooks을 사용하는 경우 아래와 같이 설정한다. useRecoilCallback()을 사용하기 위해 전달된 종속성이 잘못 지정되었을 때 경고를 표시하고 해결 방안을 제시하기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 수정된 .eslint 설정
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": [
"warn",
{
"additionalHooks": "useRecoilCallback"
}
]
}
}

React

1
2
3
4
5
npx create-react-app my-app

npm install recoil
or
yarn add recoil

RecoilRoot

부모 트리 어딘가에 RecoilRoot가 필요하다. 보통 루트 컴포넌트가 적절하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Root Component

import React from 'react';
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from 'recoil';

function App() {
return (
<RecoilRoot>
<CharacterCounter />
</RecoilRoot>
);
}

atoms

  • 상태(state)의 일부
  • 데이터를 보관하는 기본 단위
  • 업데이트와 구독이 가능하다. atom의 값을 읽는 컴포넌트들은 암묵적으로 atom을 구독한다.
  • React의 로컬 컴포넌트 상태 대신 사용할 수 있다.
  • 동일한 atom이 여러 컴포넌트에서 사용되면 모든 컴포넌트는 상태를 공유한다. atom이 업데이트 되면 각각의 구독된 컴포넌트가 새로운 값을 반영하여 다시 렌더링된다.
  • 어느 컴포넌트에서나 읽고 쓸 수 있다.
  • Redux로 따지면 Reducer와 같이 전체 store의 일부분을 차지하지만 훨씬 적은 보일러 플레이트를 차지하고 작은 단위로 관리할 수 있다.
1
2
3
4
const fontSizeState = atom({
key: 'fontSizeState',
default: 14,
});
  • key: 디버깅, 지속싱 및 모든 atoms의 map을 볼 수 있는 특정 고급 API에 사용되는 고유한 키가 필요하다.
  • key값은 전역적으로 고유해야 한다. 2개의 atom이 같은 키를 갖는 것은 오류이다.
  • React 컴포넌트의 상태처럼 기본값을 가진다.

useRecoilState

  • atom을 읽고 쓰게 하기 위해서 사용한다.
  • 컴포넌트에서 atom을 읽고 쓸 때 사용한다. React의 useState와 유사하지만 컴포넌트 간에 공유될 수 있다.는 차이점이 있다.
1
2
3
4
5
6
7
8
9
10
11
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
return (
<button
onClick={() => setFontSize((size) => size + 1)}
style={{ fontSize }}
>
Click to Enlarge
</button>
);
}

버튼을 클릭하면 버튼의 글꼴 크기가 1만큼 증가하며, fontSizeState atom을 사용하는 다른 컴포넌트의 글꼴 크기도 같이 변화한다.

1
2
3
4
function Text() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
return <p style={{ fontSize }}>This text will increase in size too.</p>;
}

atomFamily

  • writable한 recoilState atom을 반환하는 함수를 반환한다.
  • atom들의 모음집으로 저장한다. 기본적으로 Recoil 내부적으로 Caching와 같은 최적화를 진행해준다.
  • 일반적으로 atom은 RecoilRoot 단위로 등록이 되지만, 여기서 atomFamily는 사용처가 약간 다르다. 예를 들어 UI Prototyping 툴을 만들 때, 각각의 UI element에 대해 position이나 width, height와 같은 값들을 가지고 있다고 가정할 때, 이런걸 리스트로 보관하면서 memoizing해도 되지만 atomFamily를 사용하면 Recoil에서 이런 부분들을 처리해준다.

selector

  • 파생된 상태(derived state)의 일부를 나타내고 이는 상태의 변화를 의미한다.
  • 순수 함수이다.
  • 상위의 atoms나 selectors가 업데이트되면 하위의 selector 함수도 다시 실행된다.
  • 컴포넌트들은 selectors를 atoms처럼 구독할 수 있고, selectors가 변경되면 컴포넌트들도 다시 렌더링된다.
  • selectors는 상태를 기반으로 하는 파생 데이터를 계산하는 데 사용된다. 최소한의 상태 집합만 atoms에 저장하고 다른 모든 파생되는 데이터는 selectors에 명시한 함수를 통해 효율적으로 계산하는 방식으로 쓸모없는 상태의 보존을 방지한다.
  • 어떤 컴포넌트가 자신을 필요로 하는지 자신은 어떤 상태에 의존하는지 추적하기 때문에 함수적이 접근방식을 매우 효율적으로 만든다.
  • 컴포넌트의 관점에서 selectors와 atoms는 동일한 인터페이스를 가지고, 서로 대체할 수 있다.
  • atom, 다른 selector들을 조합할 수 있다.
  • 파생되는 상태를 생성한다.
  • dependency에 해당되는 atom이 업데이트되면 같이 업데이트 되기 때문에 관리의 부담이 없다.
1
2
3
4
5
6
7
8
9
const fontSizeLabelState = selector({
key: 'fontSizeLabelState',
get: ({ get }) => {
const fontSize = get(fontSizeState);
const unit = 'px';

return `${fontSize}${unit}`;
},
});
  • get: 계산될 함수. 전달되는 get 인자를 통해 다른 atoms, selectors에 접근할 수 있고, 이때 자동으로 종속 관계가 생성되므로 참조했던 다른 atoms나 selector가 업데이트되면 이 함수도 다시 실행된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
const fontSizeLabel = useRecoilValue(fontSizeLabelState);

return (
<>
<div>Current font size: ${fontSizeLabel}</div>

<button onClick={setFontSize(fontSize + 1)} style={{ fontSize }}>
Click to Enlarge
</button>
</>
);
}
  • fontSizeLabelState: selector는 fontSizeState하는 하나의 atom에 의존성을 갖는다.
    • 이 selector는 fontSizeState를 입력으로 사용하고 형식화된 글꼴 크기 레이블을 출력으로 반환하는 순수 함수처럼 동작한다.
    • useRecoilValue()를 사용해서 읽는다.
    • writable하지 않기 때문에 useRecoilState()를 이용하지 않는다.
  • 위 FontButton 예제에서 버튼을 클릭하면 버튼의 글꼴 크기가 증가하면서도 현재 글꼴 크기를 반영하도록 글꼴 크기 레이블을 업데이트하는 두 가지 작업이 수행된다.

React에서 사용하는 API

  • useRecoilValue: 기존에 사용하던 리액트의 로컬 상태 API와 동일한 형태로 사용 가능
    • atom, selector 모두 동일한 API를 사용하기에 변경이 필요할 때 언제는 Component 수정을 최소화하고 Recoil State를 변경할 수 있다.
    • 하나의 atom이나 selector를 인자로 받아 대응하는 값을 반환한다.
  • useRecoilCallback: 리액트의 useCallback과 유사하면, 다만 recoil state를 사용할 수 있는 API를
    • atom이나 selector가 업데이트 되었을 때, react componet를 리렌더하고 비동기적으로 recoil state를 읽는다.
    • Render-time에 하고 싶지 않은 시간이 오래걸리는 비동기 액션을 수행한다.
    • Recoil state를 read하거나 write하는 side-effect를 수행한다.
    • render-time에는 어떤 atom이나 selector를 업데이트하고 싶은지 알 수 없다
      • 이 경우에는 useSetRecoilState를 사용할 수 없기 떄문이다.
  • useRecoilCallback은 atom, selector state에 대한 스냅샷을 가지고 있기 때문에 특정 상태값을 사용하고 싶지만 deps에 반영하고 싶지 않을 때 유용하게 사용할 수 있다.
  • 대표적으로 logger와 같은 케이스에서 유용하게 사용할 수 있다.

왜 Recoil인가?

  • 낮은 진입장벽, 간단한 API (Redux의 Action, Store, Reducer와 같은 개념을 모두 익힐 필요 없다.)
  • React API와 굉장히 유사하여 대체하기가 용이하다.
  • 비동기 데이터 처리가 용이하다.
  • atomFamily, selectorFamily를 이용하여 국소적으로 사용하는 상태를 생성할 때 용이하다.
  • atomEffects 등을 이용하여 변화하는 값을 atom 자체적으로 트래킹할 수 있다.

Recoil의 한계점

  • atomEffect를 비롯한 많은 API가 여전히 실험적이다
  • 서버 사이드 렌더링 환경에서 안정성이 미흡하다.

References
왜 Recoil을 써야 하는가?

REST API : GraphQL 이전부터 사용.

  • URI + 요청

GraphQL은 왜 만들어졌는가?

difference between graphQL and rest API

restAPI의 문제점

  • Overfetching

💡 딱 필요한 정보들만 받아올 수는 없을까?

  • Underfetching

💡 필요한 정보들을 요청 한 번에 받아올 수는 없을까?

rest API GET = query {}
rest API POST 와 같이 서버로 데이터를 추가/수정/삭제하는 경우
mutation {}

GraphQL의 강점

  • 필요한 정보들만 선택하여 받아올 수 있음
    • overfetching 문제 해결
    • 데이터 전송량 감소
  • 여러 계층의 정보들을 한 번에 받아올 수 있음
    • underfetching 문제 해결
    • 요청횟수 감소
  • 하나의 endpoint에서 모든 요청을 처리
    • 하나의 URI에서 POST로 모든 요청 가능

Apollo

GraphQL은 명세, 형식일 뿐. GraphQL을 구현할 솔루션이 필요하다.

  • 백엔드에서 정보를 제공 및 처리
  • 프론트엔드에서 요청 전송
  • GraphQL.js, GraphQL Yoga, AWS Amplify, Relay…
  • 기타 솔루션들 살펴보기

  • Apollo는 백/프론트 모두 제공

  • 간편하고 쉬운 설정
  • 풍성한 기능들 제공

ApolloServer

  • ApolloServer : typeDef와 resolver를 인자로 받아 서버 생성
  • typeDef: GraphQL 명세에서 사용될 데이터, 요청의 타입 지정
  • gql로 생성됨
  • resolver
    • 서비스의 액션들을 함수로 지정
    • 요청에 따라 데이터를 반환, 입력, 수정, 삭제
    • GraphQL playground
      • 작성한 GraphQL type, resolver 명세 확인
      • 데이터 요청 및 전송 테스트

GraphQL 자료형

  1. 스칼라 타입
1
2
3
4
5
6
7
type EquipmentAdv {
id: ID!
used_by: String!
count: Int!
use_rate: Float
is_new: Boolean!
}
  • ID: 기본적으로는 String이나, 고유 식별자 역할임을 나타냄
  • String: UTF-8 문자열
  • Int: 부호가 있는 32비트 정수
  • Float: 부호가 있는 부동소수점 값
  • Boolean: 참/거짓 값
  1. ! : Non Null
    null을 반환할 수 없음

  2. 열거 타입
    미리 지정된 값들 중에서만 반환

  3. 리스트 타입
    특정 타입의 배열을 반환
선언부 users: null users:[] users:[…,null]
[String]
[String!]
[String]!
[String!]!
  1. 객체 타입 : 사용자에 의해 정의된 타입들
  2. union: 타입 여럿을 한 배열에 반환하고자 할 떄 사용
  3. intergace
    • 유사한 객체 타입을 만들기 위한 공통 필드 타입
    • 추상 타입 - 다른 타입에 implement 되기 위한 타입

References
Apollo Client
GraphQL

프로세스의 개념

  • 프로세스: 수행중인 프로그램
  • 프로세스 문맥(context): 프로세스가 현재 어떤 상태에서 수행되고 있는지를 정확히 규명하기 위해 필요한 정보
  • 프로세스 문맥이 중요한 이유 : 타임 쉐어링, 멀티태스킹 등 프로세스들이 번갈아가며 실행되기 때문에 프로세스의 문맥을 파악하고 있어야 이미 실행되던 프로세스를 처음부터 다시 실행한다던가 하는 문제가 생기지 않는다.

프로세스 문맥을 파악하려면 다음의 3가지 요소를 알아야 한다.

  • 프로그램 카운터가 어디를 가리키고 있는가? (code의 어느 부분까지 실행했는가?)
  • 메모리에 어떤 내용을 담고 있는가?
  • 레지스터에 어떤 값을 넣어두고, 어떤 instruction(프로그램의 기계어를 읽어서 CPU 안으로 불러들임)까지 실행했는가?

프로세스 문맥의 분류

  • 하드웨어 문맥: CPU의 수행상태. 카운터 값, 각종 레지스터에 저장하고 있는 값들
  • 프로세스의 주소 공간: 프로세스는 코드, 데이터, 스택으로 구성되는 자기만의 독자적인 주소 공간을 가지고 있고, 이것이 프로세스의 문맥을 결정짓는 중요한 요소이다.
  • 커널 상의 문맥: 프로그램이 수행되어 프로세스가 되면 프로세스를 관리하기 위한 자료 구조를 유지하게 된다. PCB, 커널 스택이 이에 해당한다.

프로세스의 상태

프로세스는 상태가 변경되며 수행되고, 항상 아래 상태 중 어느 한 상태에 머물러 있게 된다.

  • 실행 상태(running): 프로세스가 CPU를 보유하고 기계어 명령을 실행하고 있는 상태. CPU는 하나 뿐이기 때문에 여러 프로세스가 동시에 실행된다고 해도 실제 실행 상태에 있는 프로세스는 매 시점 하나 뿐이다.
  • 준비 상태(ready): 프로세스가 CPU만 보유하면 당장 명령을 실행할 수 있지만 CPU를 할당받지 못한 상태.
  • 봉쇄 상태(blocked, wait, sleep): 프로세스에게 CPU를 주어도 당장 명령을 실행할 수 없는 상태.
  • 시작 상태(new): 프로세스가 시작되어 각종 자료 구조가 생성되었지만 아직 메모리 획득을 승인받지 못한 상태
  • 완료 상태(terminated): 프로세스가 종료되었으나 운영 체제가 프로세스와 관련된 자료 구조를 완전히 정리하지 못한 상태
  • 중지(stopped, suspended): 중기 스케줄러의 등장으로 추가된 상태. 외부적인 이유로 프로세스의 수행이 정지된 상태이다.

    • 중지 준비(suspended ready): 준비 상태에 있던 프로세스가 중기 스케줄러에 의해 디스크로 스왑 아웃되면 중지 준비 상태가 된다. 외부에서 재개시키지 않는 이상 다시 활성화되지 않는다.
    • 중지 봉쇄(suspended block): 봉쇄 상태에 있던 프로세스가 중기 스케줄러에 의해 스왑 아웃된다. 이 상태에서 프로세스가 봉쇄되었던 조건을 만족하면 중지 준비 상태로 바뀐다.

    • 봉쇄 상태와 중지 상태의 차이

      • 봉쇄 상태: 자신이 요청한 이벤트가 만족되어야 Ready
      • 중지 상태: 외부에서 resume를 해주어야 Active
  1. 프로세스의 상태는 시간의 흐름에 따라 변화한다. 준비 상태에 있는 프로세스들 중에서 CPU를 할당받을 프로세스를 선택한 후 실제로 CPU의 제어권을 넘겨받는 과정을 CPU 디스패치라고 한다.
    new -> 메모리 적재 -> ready(in memory) -> CPU 획득(디스패치) -> running -> terminated
  2. 타이머 인터럽트가 발생한 경우 원래 진행하던 프로세스의 문맥을 저장하고, ready 상태에 있는 프로세스 중 하나에 새롭게 CPU 제어권을 부여하고 실행한다. 실행시킬 프로세스를 변경하기 위해 원래 수행중이던 프로세스의 문맥을 저장하고, 새로운 프로세스의 문맥을 세팅하는 과정을 문맥 교환(context switch)이라 한다.
    new -> 메모리 적재 -> ready(in memory) -> CPU 획득(디스패치) -> running -> 타이머 인터럽트 -> ready

    문맥교환

    하나의 사용자 프로세스로부터 다른 사용자 프로세스로 CPU의 제어권이 이양되는 과정

    문맥 교환의 과정

    • 원래 CPU를 보유하고 있던 프로세스가 프로세스 카운터 값 등 프로세스의 문맥을 자신의 PCB에 저장
    • 새롭게 CPU를 할당받을 프로세스가 예전에 저장했던 자신의 문맥을 PCB로부터 실제 하드웨어로 복원
    • 타이머 인터럽트나 I/O 요청으로 프로그램이 봉쇄 상태인 경우 문맥 교환이 발생하지만 그 밖의 인터럽트나 시스템 콜 발생 시에는 모드 변경만 있다. 프로세스의 실행 모드가 사용자에서 커널로 변경된 것일 뿐, CPU를 점유하는 프로세스가 다른 사용자 프로세스로 변경되는 것이 아니기 때문이다.
    • 프로세스 간 문맥 교환이 빈번해지면 오버헤드도 상당히 커진다.

      • 문맥 교환이 발생하지 않는 경우
        user mode(사용자 프로세스 A) -> interrupt or system call -> kernel mode(ISR or system call 함수) -> -> 문맥 교환 없이 user mode 복귀 -> user mode(사용자 프로세스 A)

      • 문맥 교환이 발생하는 경우
        user mode(사용자 프로세스 A) -> timer interrupt or I/O 요청 system call -> kernel mode -> 문맥 교환 발생 -> user mode(사용자 프로세스 B)

  3. I/O 요청이 발생한 경우에도 실행 상태에 있던 프로세스가 봉쇄 상태로 바뀌는 문맥 교환이 발생한다.
    new -> 메모리 적재 -> ready(in memory) -> CPU 획득(디스패치) -> running -> I/O 또는 사건 대기 -> waiting(blocked) -> I/O 또는 사건 완료 -> ready


프로세스 제어 블록 (PCB: Process Control Block)

운영 체제가 시스템 내의 프로세스들을 관리하기 위해 프로세스당 유지하는 정보를 담는 커널 내의 자료 구조

1.
OS가 사용하는 정보
pointer process state
process number
2.
CPU 수행 관련 하드웨어 값
process counter
registers
3.
메모리 관련
memory limits
4.
파일 관련
open files
...

프로세스를 스케줄링하기 위한 큐

프로세스는 각 큐를 오가며 수행한다.

  • 작업 큐(job queue): 시스템 내 모든 프로세스를 관리하기 위한 큐. 프로세스 상태와 무관하게 모든 프로세스 상태가 속하지만 작업 큐에 있다고 해서 반드시 메모리를 가진 것은 아니다.
  • 준비 큐(ready queue): CPU를 할당받고 실행되기 위해 기다리고 있는 프로세스의 집합. 프로세스는 준비 상태
  • 장치 큐(device queue): 각각의 장치마다 서비스를 기다리며 줄 서 있는 프로세스의 큐. 프로세스는 봉쇄 상태

스케줄러

어떤 프로세스에게 자원을 할당할지를 결정하는 운영 체제 커널의 모듈

  • 장기 스케줄러(job scheduler): 어떤 프로세스를 준비 큐에 삽입할지를 결정한다. 준비 큐는 CPU만 얻으면 당장 실행 가능한 프로세스이기 때문에 메모리를 보유해야 하고, 메모리 또는 각종 자원을 얼마나 할당할지에 대해서도 관여한다.
  • 수십 초 내 수 분 단위로 가끔 호출되므로 상대적으로 속도가 느려도 된다.
  • 메모리에 동시에 올라가 있는 프로세스의 수를 조절한다.
  • 현대 시분할 시스템에서는 보통 장기 스케줄러가 없다.

중기 스케줄러: 현대 시분할 시스템용 운영체제에서 사용한다. 너무 많은 프로세스에게 메모리를 할당해 시스템의 성능이 저하될 때 메모리에 적재된 프로세스의 수를 동적으로 조절한다.
프로세스당 보유 메모리량이 지나치게 적으면 일부 프로세스를 메모리에서 디스크로 스왑 아웃시킨다.

  • 단기 스케줄러(CPU scheduler): 준비 상태의 프로세스 중에서 어떤 프로세스를 다음 번에 실행 상태로 만들 것인지 결정한다. 준비 큐에 있는 프로세스 중 어떤 프로세스에게 CPU를 할당할지를 결정하고, 시분할 시스템에서는 타이머 인터럽트가 발생하면 단기 스케줄러가 호출된다.
  • ms 이하 단위로 매우 빈번하게 호출되므로 속도가 빨라야 한다.

프로세스의 생성

시스템 부팅 후 최초의 프로세스는 운영 체제가 생성하고, 그 다음부터는 이미 존재하는 프로세스가 다른 프로세스를 복제 생성

  • 부모 프로세스: 프로세스를 생성한 프로세스
  • 자식 프로세스: 새롭게 생성된 프로세스

부모 프로세스가 자식 프로세스를 생성하는 방식으로 족보(Tree)와 같은 계층 구조를 형성한다.

프로세스의 작업 수행

프로세스가 작업을 하려면 자원이 필요한데 아래와 같은 세가지 유형으로 자원을 공유한다.

  • 부모-자식이 모든 자원을 공유
  • 일부를 공유
  • 전혀 공유하지 않음

프로세스가 수행되는 모델

  • 부모-자식이 공존하며 수행: 자식과 부모가 CPU를 획득하기 위해 경쟁하는 관계.
  • 자식이 종료될 때까지 부모가 기다림: 자식 프로세스가 종료될 때까지 부모 프로세스는 봉쇄 상태에 있다가 자식 프로세스가 종료되면 부모는 준비 상태가 되어 다시 CPU를 얻을 권한이 생긴다.

프로세스의 생성 절차

프로세스는 생성되면 자신만의 독자적인 주소 공간을 갖고, 자식 프로세스는 부모 프로세스의 주소 공간 내용을 그대로 복사해서 생성한다.

  • 생성: 유닉스에서는 fork() 시스템 콜로 새로운 프로세스를 생성하고, 자식 프로세스를 생성할 때 부모 프로세스의 내용을 그대로 복제 생성한다. 부모 프로세스의 모든 문맥을 복제해서 생성되었기 때문에 부모 프로세스가 현재 수행한 시점(프로그램 카운터 지점)부터 수행할 수 있다.

  • 종료: 부모 프로세스 종료 전에 자식 프로세스부터 종료되어야 한다.

    • 자발적 종료: 프로세스가 마지막 명령 수행 후 exit() 시스템 콜로 운영 체제에게 알린다.
    • 비자발적 종료: 부모 프로세스가 abort()를 호출하여 자식 프로세스의 수행을 강제 종료시킨다.

프로세스 간의 협력

프로세스는 각자 자신의 독립적인 주소 공간을 가지고 수행하므로 원칙적으로 하나의 프로세스는 다른 프로세스의 수행에 영향을 미칠 수 없다.

하지만 독립적인 프로세스들이 서로 협력할 때 효율적인 경우 협력 매커니즘을 제공하여 하나의 프로세스가 다른 프로세스의 수행에 영향을 미칠 수 있도록 한다.

IPC(Inter-Process Communication)

대표적인 협력 매커니즘으로 하나의 컴퓨터 안에서 실행중인 서로 다른 프로세스 간에 발생하는 통신. 의사 소통 기능과 동기화가 보장되어야 한다.

  • 메시지 전달: 프로세스 간 공유 변수를 사용하지 않고, 커널을 통해 메시지를 전달하는 방법으로 통신
  • 공유 메모리: 프로세스 간 공유 변수를 사용하여 주소 공간의 일부를 공유한다.

스레드

스레드는 하나의 프로세스이므로 프로세스 간 협력으로 보기는 어렵지만 동일한 프로세스를 구성하는 스레드 간에는 주소 공간을 공유하므로 협력이 가능하다.

  • program counter, register set, stack space로 구성된다.
  • 스레드가 동료 스레드와 공유하는 부분(task): code section, data section, OS resource

References
운영체제
[운영 체제와 정보 기술의 원리] 반효경 지음

CPU: 프로그램의 기계어 명령을 실제로 수행하는 컴퓨터 내의 중앙 처리 장치

CPU는 일반적으로 시스템 내에 하나 뿐이기 때문에 여러 프로그램이 동시에 수행되는 시분할 환경에서 매우 효율적으로 관리되어야 한다.

프로그램 실행에는 CPU 내에서 수행되는 기계어 명령은 다음의 세 가지가 있다.

  1. CPU 내에서 수행되는 명령
  2. 메모리 접근을 필요로 하는 명령
  3. 입출력을 동반하는 명령
  • CPU 버스트(CPU burst): 1,2는 사용자 프로그램이 직접 CPU를 가지고 수행하는 비교적 빠른 명령. 프로그램이 I/O를 한 번 수행한 후 다음번 I/O를 수행하기까지 직접 CPU가지고 명령을 수행하는 일련의 작업이다.
  • I/O 버스트: 3은 I/O 요청이 발생해 커널에 의해 입출력 작업을 진행하기 때문에 비교적 느린 명령이다. I/O 작업이 요청된 후 완료되어 다시 CPU 버트스토 돌아가기까지 일어나는 일련의 작업이다.

각 프로그램마다 CPU버스트와 I/O 버스트가 차지하는 비율은 균일하지 않지만 아래와 같이 프로세스를 분류해볼 수 있다.

  • I/O 바운드 프로세스: I/O 요청이 빈번해서 CPU 버스트가 짧게 나타난다. e.g. 대화형 프로그램
  • CPU 바운드 프로세스: I/O 작업을 거의 수행하지 않아 CPU 버스트가 길게 나타난다. e.g. 계산위주 job

CPU는 이와 같이 사용하는 패턴이 상이한 여러 프로그램이 동일한 시스템 내에서 실행되기 때문에 효율적인 스케줄링이 매우 중요하다. CPU 스케줄링 시 CPU 버스터가 짧은 프로세스(I/O 바운드 프로세스)에게 우선적으로 CPU를 사용할 수 있도록 한다. CPU 바운드 프로세스를 먼저 CPU에 할당하면 그 프로세스가 CPU를 다 사용할 때까지 I/O 바운드 프로세스의 응답 시간이 길어지고, 해당 I/O 장치도 그 시간동안 작업을 수행하지 않기 때문이다.


CPU 스케줄러

준비 상태에 있는 프로세스 중 어떠한 프로세스에게 CPU를 할당할지 결정하는 운영 체제의 코드

스케줄링 방식

  1. 비선점형 방식(nonpreemptive): CPU를 획득한 프로세스가 스스로 CPU를 반납하기 전까지 CPU를 빼앗기지 않음.
    1. 실행 상태 -> I/O 요청 -> blocked
    2. CPU에서 실행 중이던 프로세스 종료
  2. 선점형 방식(preemptive): 프로세스가 CPU를 계속 사용하기 원하더라도 강제로 빼앗을 수 있음.
    1. 실행 상태 -> 타이머 인터럽트 -> Ready
    2. I/O 요청 -> 봉쇄 -> I/O 작업 완료 -> 인터럽트 -> Ready

디스패치

CPU 스케줄러가 어떤 프로세스에세 CPU를 할당할지 결정하고 나면 새롭게 선택된 프로세스가 CPU를 할당받고 작업을 수행할 수 있도록 환경 설정을 하는 커널 모듈

스케줄링의 성능 평가

  • 시스템 관점: 시스템 입장에서의 성능 척도. CPU 활용도와 처리량
  • 사용자 관점: 프로그램 입장에서의 성능 척도. 소요 시간, 대기 시간, 응답 시간 등 기다린 시간과 관련된 지표
  1. CPU 활용도: 전체 시간 중 CPU가 명령을 수행한 시간의 비율. 휴면(idle) 상태에 머무르는 시간을 최대한 줄이는 것이 중요하다.
  2. 처리량: 주어진 시간 동안 CPU 버스트를 완료한 프로세스의 개수. CPU 버스트가 짧은 프로세스에게 우선적으로 할당하는 것이 유리하다.
  3. 소요 시간: 프로세스가 CPU 요청 시점부터 CPU 버스터가 끝날 때까지 걸린 시간. 준비 큐에서 기다린 시간 + 실제로 CPU를 사용한 시간
  4. 대기 시간: 프로세스가 CPU 버스트 기간 중 준비 큐에서 기다린 시간의 합
  5. 응답 시간: 프로세스가 CPU 요청 시점부터 처음으로 CPU를 얻을 때까지 걸린 시간. 시분할 환경에서 매우 중요함.

스케줄링 알고리즘

1. FCFS (First-Come First-Served)

프로세스가 준비 큐에 도착한 시간 순서대로 CPU를 할당

비선점형으로 먼저 요청한 프로세스가 자발적으로 CPU를 반납할 때까지 선점하지 않는다. 따라서 먼저 도착한 프로세스가 작업 시간이 길 경우 다수의 프로세스들이 앞 작업이 끝날 때까지 기다려야 해서 평균 대기 시간이 길어질 수 있다. 이렇게 CPU 버스트가 긴 프로세스 다음에 짧은 프로세스가 도착해서 오랜 시간을 기다려야 한다면 이를 콘보이 현상이라고 한다. 이런 경우 I/O 장치의 활용도까지도 떨어지게 된다. 그래서 짧은 프로세스가 도착하면 평균 대기 시간은 짧아지고, 프로세스의 성격에 따라 긴 프로세스가 먼저 도착하면 평균 대기 시간이 길어진다.

2. SJF (Shortest-Job-First)

CPU 버스트가 가장 짧은 프로세스에게 제일 먼저 CPU를 할당

평균 대기 시간을 가장 짧게 하는 최적을 알고리즘이다.

  • 비선점형 방식: 현재 CPU에서 실행중인 프로세스의 남은 CPU 버스트 시간보다 더 짧은 프로세스가 도착하면 CPU를 빼앗긴다. (SRTF: Shortest Remaining Time First)
  • 선점형 방식: 현재 CPU를 점유하고 있는 프로세스가 CPU 버스트를 모두 수행하고 스스로 CPU를 내어놓을 때까지 스케줄링을 하지 않는다.

시분할 환경에서는 중간중간 새로운 프로세스가 도착하는 경우가 발생하기 때문에 선점형 방식이 평균 대기 시간을 가장 많이 줄일 수 있다. 하지만 프로세스의 CPU 버스트 시간을 미리 알 수 없기 때문에 과거의 CPU 버스트 시간을 통해 시간을 예측해서 프로세스에 CPU를 할당한다. 그런데 어떠한 프로세스가 시작되었는데, 그 다음으로 CPU 버스트가 짧은 프로세스가 계속 도착해서 CPU를 빼앗기게 되고, 짧은 프로세스가 계속 도착하게 되면 처음 프로세스는 영원히 CPU를 할당받을 수 없을 수 있고, 이를 기아(starvation) 현상이라 한다.

3. 우선순위 스케줄링

준비 큐에서 기다리는 프로세스들 중에 우선순위가 가장 높은 프로세스에게 제일 먼저 할당

우선순위는 우선순위값을 통해 표시해서 그 값이 작을수록 높은 우선순위를 가지는 것이다. 우선순위 스케줄링도 SJF처럼 기아 현상이 있을 수 있는데, 우선순위가 높은 프로세스가 계속 도착하면 우선순위가 낮은 프로세스는 CPU를 얻지 못하고, 계속 대기해야 하기 때문이다. 이를 해결하기 위해서 노화 기업(aging)을 사용하고, 이는 기다리는 시간이 길어지면 우선순위를 조금씩 높여 언젠가는 가장 높은 우선순위가 되어 CPU를 할당받을 수 있게 하는 것이다.

4. 라운드 로빈 스케줄링

시분할 시스템의 성질을 가장 잘 활용한 새로운 의미의 스케줄링 방식

각 프로세스가 CPU를 연속적으로 사용할 수 있는 시간이 제한되며, 시간이 경과한 프로세스가 있으면 CPU를 회수해서 준비 큐에 있는 다른 프로세스에게 CPU를 할당한다. 각 프로세스마다 한 번에 CPU를 연속적으로 사용할 수 있는 최대 시간은 할당 시간이라 부른다. 할당시간이 너무 길면 FCFS처럼 콘보이 현상이 발생할 수 있고, 반대로 너무 짧으면 CPU 프로세스 교체가 빈번해서 문맥 교환에 오버헤드가 커진다. 따라서 할당 시간은 수십 밀리세컨드 정도의 규모로 설정한다. 라운드 로빈은 여러 종류의 이질적인 프로세스가 같이 실행되는 환경에서 효과적이고, 대화형 프로세스의 빠른 응답 시간을 보장할 수 있다는 장점이 있다.


References
운영체제
[운영 체제와 정보 기술의 원리] 반효경 지음

Introduction to Operating Systems

컴퓨터 분야의 학문은 아래의 두 가지 분야로 나뉘지만 복잡도(complexity)가 매우 높은 문제를 다루고 있어서 방법론적인 차원에서는 크게 다르지 않다.

  • 컴퓨터 자체를 효율적으로 운영하기 위한 학문
  • 복잡한 문제를 컴퓨터를 활용하여 효율적으로 풀 수 있는 방법을 제공하기 위한 방법

컴퓨터가 일을 처리하는 방식은 정확한 처리 방식을 알고리즘이라는 형식을 통해 기술해 주어야 하고, 작업 내용의 복잡도가 매우 높아서 사람이 하는 것처럼 눈썰미나 직감으로 처리할 수 없다. 특히 데이터의 수가 많아질수록 효율적으로 작업을 수행할 수 있도록 하는 체계적인 방법이 필요하다. 그 체계적인 방법 중 하나가 컴퓨터 하드웨어와 스프트웨어를 총체적으로 관리하는 핵심 소프트웨어인 운영체제이다.

컴퓨터 및 정보 기술의 역사

1. 이론적 기원

수학과 논리학에서 컴퓨터의 이론적 기원을 찾아볼 수 있다. 어떠한 문제를 수학적인 모델로 표현하는 방법을 개발하고, 그 문제를 풀기 위한 알고리즘을 기술할 수 있는 컴퓨터에 대한 추상적인 모델을 설계했다. (e.g. 튜링 머신)

2. 기계식 컴퓨터

컴퓨터는 계산을 빠르게 하기 위해 개발되었다. 19세기 해석기관(analytic engine)는 프로그램이 가능한 최초의 기계식 컴퓨터이다. 베비지가 설계한 이 컴퓨터는 현대의 컴퓨터에서 발견되는 네 가지 기본 구성 요소인 입력 장치, 출력 장치, 처리 장치, 저장 장치를 포함하고 있다.

3. 전자식 컴퓨터

20세기 초에 전자식 계산기가 등장하고, 이는 전자 장치에 의해 동작하는 본격적인 의미의 컴퓨터이다. ABC, Mark1, ENIAC 등이 만들어졌다. 특히 ENIAC은 최초의 현대적 컴퓨터로 인식된다. 이를 기점으로 컴퓨터 역사의 경계를 나누기도 한다.

4. 근대적 컴퓨터

근대로 넘어오면 변화가 매우 빨라져서 시대적 분류가 쉽지 않지만 1940년대 중빈부터 하드웨어 기술 발전을 토대로 대체로 1~4세대로 분류한다.

  • 1세대 컴퓨터: 1940년대 후반 시작된 진공관 기반 컴퓨터
  • 2세대 컴퓨터: 1950년대 후반 시작된 트랜지스터 기반 컴퓨터
  • 3세대 컴퓨터: 1960년대 후반 시작된 직접회로 기반 컴퓨터
  • 4세대 컴퓨터: 1970년대 중반 시작된 LSI(Large Scale Integration), VLSI(Very Large Scale Integration) 기반 마이크로 컴퓨터

2세대부터 소프트웨어의 발전이 크게 이루어지고, 컴퓨터의 사용이 확산되면서 프로그래밍의 필요성도 크게 증가한다. 그에 따라 기계어로 프로그래밍을 하는 불편함 때문에 사람이 프로그래밍 하기 수월한 언어의 필요성이 대두되었고, 어셈블리 언어가 등장한다. 그외 고급 언어인 포트란, 리습(Lisp) 언어, 코볼(Cobol) 등이 개발된다. 1960년대 이후에는 설계의 방법론이라 할 수 있는 소프트웨어 공학이 부각되면서 구조적 프로그래밍 기법이 부각된다. 또한 알골 60(Algol 60)이라는 언어가 등장했고, 운영 체제가 개발되기 시작한다. 그 이유는 초기에는 컴퓨터 외부에서 미리 예약해서 한꺼번에 처리하는 일괄 처리 방식(batch processing)을 사용했는데, 그것이 비효율적이기 때문에 컴퓨터 자체가 이런한 것을 자동적으로 처리해 주도록 하는 방식을 생각하여 운영 체제가 생겨났다. 컴퓨터의 응용 분야로는 경영 자동화가 부각되면서 데이터베이스 관리 시스템(DBMS: DataBase Management System)도 등장한다. 1960년대 중반부터는 특히 반도체 기술의 빠른 발전으로 인해 컴퓨터 하드웨어에 큰 변화가 있었고, 1970년대에 들어서면서 하드웨어와 소프트웨어의 설계 방법론 측면이 크게 부각된다. 그 이유는 하드웨어가 반도체 기술의 발전과 그로 인한 직접 회로(IC)의 발전으로 상당한 성능 향상이 있었지만 소프트웨어쪽은 그다지 만족한 말한 발전을 이루지 못했기 때문이다. 하지만 이런 하드웨어의 고도화로 개인용 컴퓨터가 등장하고, 컴퓨터 네트워크에 대한 발전의 기초를 마련하게 된다. 이 시기에는 C언어가 개발되었다. 또한 마이크로프로세서가 직접회로를 더욱 고도화한 초고밀도 직접회로(VLSI)기술로 제작되면서 4세대 컴퓨터 시대를 열게 된다. 이 마이크로프로세서의 보급이 개인용 컴퓨터 혁명을 야기하여 1960년대 후반부터는 애플, 코모도, 탠디 등의 회사가 개인용 컴퓨터의 생산을 시작한다. 1980년대에 소프트웨어 방법론이 많이 등장하게 되고, 객체 지향 언어가 성공한다. 1990년대 초반부터는 컴퓨터가 사회 전반으로 뿌리내리게 되면서 인텔 펜티엄 프로세서, 윈도우 95, 월드 와이드 웹, 자바 등이 등장한다.

오늘날 현대의 컴퓨터는 규모에 따라 여러 가지로 나뉠 수 있는데, 보통 임의의 목적으로 사용될 수 있는 것을 범용 컴퓨터라고 부르고, 특수 목적을 위해 각종 장치의 제어용으로 내장되는 컴퓨터를 임베디드 컴퓨터라고 부른다.

슈퍼 컴퓨터, 메인 프레임 컴퓨터

마이크로프로세서 등장 이전의 컴퓨터는 대부분 커다란 크기의 메인 프레임 컴퓨터였다.

  • 메인 프레임 컴퓨터: 일반적으로 터미널을 통해 접속한다. 시분항 방식(컴퓨터 처리 능력을 짧은 시간 단위로 구분하여 여러 사용자에게 조금씩 분할해서 서비스)을 사용한다.
  • 슈퍼 컴퓨터: 메인 프레임 컴퓨터로 처리 능력이 부족한 응용 분야에 쓰인다. (e.g. 기상 예측, 통신망 설계, 석유 탐사 등) 복잡한 문제를 다루기 때문에 처리 능력이 메인 프레임 컴퓨터보다 뛰어나야 하기 때문에 슈퍼 컴퓨터 또는 고성능 컴퓨터라고도 부른다.

개인용 컴퓨터

메인 프레임 컴퓨터를 사용하기에는 규모가 작은 연구실이나 사무실에서 쓰는 워크스테이션의 개념이 등장한다. 이는 10인 이내의 구성원이 공동으로 사용하기에 적절한 컴퓨터를 의미한다. 최근에는 개인용 컴퓨터의 성능이 좋아져서 워크스테이션과 개인용 컴퓨터의 격차가 많이 사라졌다.

휴대용 컴퓨터

데스크탑 컴퓨터와 달리 휴대가 가능한 컴퓨터를 휴대용 컴퓨터라 하는데, 랩탑 컴퓨터가 있다. 랩탑보다 더 작은 사이즈의 스마트 폰도 있다.

임베디드 컴퓨터

특수한 목적을 가지고 제작되는 컴퓨터로 각종 기기에 포함되어 그 기능을 향상시키거나 연산, 처리, 전달 등의 업무를 담당한다. 칩이 내부에 구워져 있어서 범용 컴퓨터와 같은 일반적인 방법으로 프로그램을 올릴 수 없다. 용도의 특수성으로 인해서 한번 기록된 프로그램이 수정될 일이 거의 없기 때문이다.


운영 체제 개요

운영체제: 컴퓨터 하드웨어 바로 윗단에 설치되는 소프트웨어. 사용자 및 다른 모든 소프트웨어와 하드웨어를 연결하는 소프트웨어 계층

운영 체제가 없으면 컴퓨터는 고철 덩어리에 불가하다. 하드웨어가 운영 체제와 한 몸이 되어야만 사용자에게 쓰일 수 있는 진정한 컴퓨터 시스템이 된다. 사용자가 하드웨어 자체를 다루는 것이 쉽지 않으므로 하드웨어 위에 운영체제를 탑재하여 전원을 켰을 때 사용자가 손쉽게 사용할 수 있는 상태가 되도록 하는 것이다. 컴퓨터의 전원을 켜면 운영 체제도 켜지는 셈이다.

운영 체제의 기능

  • 핵심 기능: 컴퓨터 시스템 내의 자원을 효율적으로 관리하여 가장 좋은 성능을 내도록 한다.
    • 자원의 효율적 관리가 매우 중요하기 때문에 자원 관리자(resource manager)라 부르기도 한다.
    • 자원: CPU, 메모리, 하드 디스크, 프로세스, 파일, 메시지 등 하드웨어와 소프트웨어 자원을 통칭한다.
    • 단, 효율성 추구로 인해 일부가 지나치게 희생되지 않도록 하는 형평성의 문제도 고려해야 한다.
    • (사용자 -> 프로그램 -> 추상화된 컴퓨터(Abstract Machine) -> 운영체제(에 의한 자원공유) -> 물리적인 컴퓨터 -> 결과(Result))
  • 컴퓨터 시스템을 편리하게 사용할 수 있는 환경 제공
    • 운영 체제가 동시 사용자 및 프로그램들에게 각각 독자적으로 컴퓨터를 사용하는 것과 같은 환상을 제공한다.
    • 하드웨어를 직접 다루는 복잡한 부분을 운영 체제가 대행하고, 사용자 및 프로그램은 이에 대해 자세한 내용은 알지 못해도 프로그램을 계속 수행할 수 있다. (사용자 -> 프로그램 -> 추상화된 컴퓨터(Abstract Machine) -> 물리적인 컴퓨터 -> 결과(Result))
  • 사용자와 운영체제 자신을 보호

운영 체제의 분류

1. 동시 작업을 진행하는 지 여부에 따라 분류한다.

  • 단일 작업용 운영 체제: 한 번에 하나의 프로그램만 수행시킨다.
  • 다중 작업: 동시에 두 개 이상의 프로그램을 처리할 수 있다.

다중 작업용 운영 체제의 개념은 잘 구분해서 정리해야 한다. 운영 체제가 다중 작업을 처리할 때 여러 프로그램이 CPU와 메모리를 공유하는 데, 일반적으로 컴퓨터에는 CPU가 하나 밖에 없다. 따라서 다중 작업용 운영 체제라도 CPU에서는 매 순간 하나의 프로그램만 수행된다.

  • 시분할 시스템(time sharing system): CPU에서 수 밀리 세컨드(ms)이내에 여러 프로그램들이 번갈아가면서 수행된다. 따라서 사용자는 여러 프로그램이 동시에 수행되는 것처럼 느끼게 된다.
  • 다중 프로그래밍 시스템(multi-programming system): 메모리 공간을 분할해 여러 프로그램들을 동시에 메모리에 올려놓고 처리한다. 대화형 시스템(interactive system)이라고도 부른다.

다중 작업, 시분할, 다중 프로그래밍은 모두 여러 프로그램이 하나의 컴퓨터에서 동시에 수행된다. 요즘의 운영체제는 대게 이러한 방식이다.

  • 다중처리기 시스템(multi-processor system): 하나의 컴퓨터 안에 CPU가 여러 개 설치된 경우를 말한다.

2. 다중 사용자의 동시 지원 여부 (사용자의 수에 따른 분류)

  • 단일 사용자용 운영 체제: 한 번에 한 명의 사용자만이 사용하도록 허용한다. e.g. MS_DOS, MS windows
  • 다중 사용자용 운영 체제: 여러 사용자가 동시에 접속해 사용할 수 있게 한다. e.g. UNIX, NT server

3. 작업 처리 방식에 따른 분류

  • 일괄 처리 방식(batch processing): 작업 요청의 일정량을 모아서 한꺼번에 처리한다. e.g. 펀치카드
  • 시분할 방식(time sharing): 여러 작업을 수행할 때 컴퓨터의 처리 능력을 일정한 시간 단위로 분할하여 사용한다. e.g. UNIX
  • 실시간 운영 체제(real time): 정해진 시간 안에 어떠한 일이 반드시 종료됨이 보장되어야 한다. e.g. 원자로, 공장 제어 시스템, 미사일 제어 시스템 등

운영 체제의 자원 관리 기능

운영 체제의 가장 핵심적인 기능은 자원을 효율적으로 관리. 자원은 CPU, 메모리 등을 비롯한 주변 장치 및 입출력 장치 등의 하드웨어 자원소프트웨어 자원으로 나뉜다.

하드웨어 자원 관리

  • CPU 스케줄링: 어떤 프로그램에게 CPU를 줄 것인가? CPU는 하나이기 때문에 매 시점 어떤 프로세스가 CPU를 할당해 작업을 처리할지 결정해야 한다.
    • 선입 선출 기법: 먼저 CPU를 사용하기 위해 도착한 프로세스를 먼저 처리해준다. e.g. 줄서기
    • 라운드 로빈 기법: CPU를 한 번 할당받아 사용할 수 있는 시간을 일정한 고정된 시간으로 제한한다. 긴 작업을 요하는 프로세스가 CPU를 할당 받더라도 정해진 시간이 지나면 CPU를 내어놓아야 한다. 선입 선출 기법이 장시간 처리가 필요한 프로세스가 처리 될 동안 다른 프로세스들은 장시간 처리가 끝날때까지 기다려야 해서 전체 시스템상의 비효율이 발생하기 때문에 이를 보완하기 위한 방법으로 고안되었다.
    • 우선 순위 스케줄링: 수행 대기중인 프로세스들에게 우선순위를 부여하여 우선순위가 높은 프로세스에게 CPU를 먼저 할당한다.
  • 파일 관리: 디스크에 파일을 어떻게 보관할 것인가? CPU, 메모리는 전원이 꺼지면 처리중이던 정보가 모두 꺼지므로 전원이 꺼져도 기억이 필요한 부분은 보조 기억 장치에 파일 형태로 저장한다. 이 파일들이 저장되는 방식 및 접근 권한 등을 OS가 관리해주어야 한다.
  • 입출력 관리: 각기 다른 입출력 장치와 컴퓨터 간에 어떻게 정보를 주고 받을 것인가? 키보드, 모니터, 하드 디스크 등
    • 인터럽트: 주변 장치들이 CPU의 서비스가 필요한 경우 신호를 발생시켜 서비스를 요청하는데 이 신호를 인터럽트라고 한다. 그러면 CPU는 하던 작업을 멈추고 인터럽트가 요청한 서비스를 수행한다.
    • 컨트롤러: 주변 장치들이 각 장치마다 해당 장치에서 일어나는 업무에 대한 관리를 위한 일종의 작은 CPU를 가지고 있는데, 이를 컨트롤러라고 한다. 해당 장치에 대한 업무를 처리하고, 메인 CPU에 인터럽트를 발생시켜 보고하는 역할을 한다.
  • 메모리 관리: 한정된 메모리를 어떻게 나누어 사용할 것인가? 메모리의 어느 부분이 어떤 프로그램에 의해 사용되고 있는지를 주소(address)를 통해 관리하고, 메모리가 필요할 때 할당하고, 그렇지 않을 때 회수한다.
    • 고정 분할 방식: 물리적 메모리를 몇 개의 영구적인 분할로 나눈다. 동시에 메모리에 적재되는 최대 프로그램의 수가 분할 개수로 한정되어 융통성이 없다는 단점이 있고, 분할의 크기보다 큰 프로그램은 적재가 불가하다.
      • 내부 조각: 분할의 크기보다 작은 프로그램이 적재되었을 때 남는 영역을 말한다. 사용되지 않는 비효율적인 공간이다.
    • 가변 분할 방식: 매 시점 프로그램의 크기에 맞게 메모리를 분할한다.(분할의 크기, 개수에 따른 동적인 분할) 물리적 메모리 크기보다 더 큰 프로그램은 적재가 불가하다.
      • 외부 조각: 프로그램에게 할당되지 않은 메모리 영역인데, 크기가 작아서 프로그램을 올리지 못하는 메모리 영억. 사용되지 않는 비효율적인 공간이다.
    • 가상 메모리 방식: 최근의 거의 모든 컴퓨터 시스템에서 사용한다. 물리적 프로그램보다 더 큰 프로그램의 실행을 지원한다. 모든 프로그램은 물리적 메모리와 독립적인 주소가 0부터 시작하는 자신만의 가상 메모리를 갖는다. OS는 가상 메모리의 주소를 물리적 메모리 주소로 매핑하는 기술을 이용해서 주소를 변환시켜서 프로그램을 물리적 메모리에 올린다. 따라서 실행될 수 있는 프로그램의 크기는 가상 메모리 크기에 의해 결정된다.
      • 스왑 영역(swap area): 현재 사용되고 있는 부분만 메모리에 올리고, 나머지는 하드 디스크와 같은 보조 장치에 저장했다가 필요할 때 적재하는 방식으로 이때 사용되는 보조 기억 장치의 영역이 스왑 영역이다.

References
운영체제
[운영 체제와 정보 기술의 원리] 반효경 지음

컴퓨터 시스템의 구조

컴퓨터의 업무 처리 방식: 컴퓨터 외부 장치에서 컴퓨터 내부로 데이터를 읽어와서 각종 연산을 수행 후, 그 결과를 컴퓨터 외부 장치로 다시 내어 보내는 방식. 컴퓨터 내부로 데이터가 들어오는 것이 입력(input), 외부 장치로 데이터가 나가는 것은 출력(output)이다.

  • 입출력 장치: 컴퓨터 외부 장치를 입출력 장치라고도 한다.
  • 컨트롤러: 컴퓨터 내의 하드웨어 장치에 존재하는 일종의 작은 CPU
  • 커널: 운영 체제 중 항상 메모리에 올라가 있는 부분

CPU와 각종 컨트롤 디바이스의 모습

CPU와 I/O 연산

  • CPU: 컴퓨터 내에서 수행되는 연산을 담당한다. 매시점 메모리에서 명령을 하나씩 읽어와서 수행한다.
    • 인터럽트 라인: CPU 옆에 있는데, CPU가 자신의 작업을 하던 중 인터럽트 라인에 신호가 들어오면 하던 일을 멈추고 인터럽트와 관련된 일을 처리한다. 작업이 다 끝났는지 어떤지 전달받는 역할을 한다.
  • I/O 연산: 입출력 장치의 I/O 연산은 I/O 컨트롤러가 담당한다.

    • 컨트롤러: 입출력 장치와 메인 CPU는 동시 수행이 가능한데, 이를 제어하기 위해 장치 컨트롤러가 있다. 키보드 입력 등의 이벤트를 CPU에게 알려 줄 필요가 있는 경우 컨트롤러가 발생시킨다.
      • 디스크나 키보드 등의 장치에서 로컬 버퍼로 데이터를 읽어오는 일을 담당한다.
      • 인터럽트를 발생시켜 CPU에 보고하여 데이터를 모두 가지고 왔는지 아닌지를 체크한다.
    • 로컬 버퍼(local buffer): 장치로부터 들어오고 나가는 데이터를 임시로 저장하기 위한 작은 메모리. 로컬 버퍼에 임시 저장되어 있던 데이터는 이후 메모리에 전달된다.
  • 사용자 프로그램은 어떻게 I/O를 할까?

    • System call: 사용자 프로그램은 운영체제에게 I/O 요청
    • trap을 이용하여 이너럽트 벡터의 특정위치로 이동
    • 제어권이 인터럽트 벡터가 가리키는 인터럽트 서비스 루틴으로 이동
    • 올바른 I/O 요청인지 확인 후 수행
    • I/O 완료 시 제어권을 시스템 콜 다음 명령으로 옮긴다.

인터럽트: 인터럽트 당한 시점의 레지스터와 Program Counter를 save한 후 CPU의 제어를 인터럽트 처리 루틴에 넘긴다.

인터럽트의 일반적인 기능

일반적으로 인터럽트는 하드웨어가 발생시킨 인터럽트를 뜻하지만 넓은 의미의 인터럽트는 하드웨어와 소프트웨어 인터럽트 모두를 말한다. 두 가지의 차이점은 일을 수행하는 방식에 있어서 하드웨어 인터럽트는 컨트롤러 등 하드웨어 장치가 CPU의 인터럽트 라인을 세팅하는 방식이고, 소프트웨어 인터럽트는 소프트웨어가 그 일을 수행한다.

  • 하드웨어 인터럽트: 하드웨어가 발생시킨 인터럽트
  • 소프트웨어 인터럽트(Trap)
    • exception: 프로그램이 오류를 범한 경우
    • system call: 프로그램이 커널 함수를 호출한 경우. 운영체제에 정의된 함수를 호출하는 것.

인터럽트 핸들링

인터럽트가 발생한 경우 처리해야 할 일의 절차

하드웨어 인터럽트

인터럽트 관련 용어

  • 인터럽트 처리 루틴(interrupt service routine): 인터럽트 핸들러. 커널 내에 존재하는 해당 인터럽트를 처리하는 커널 함수. 다양한 인터럽트들이 각각 처리해야 할 업무들을 정의하고 있다.
  • 인터럽트 벡터: 여러 가지 인터럽트에 대해 해당 인터럽트 발생시 처리해야 할 루틴의 주소를 보관하고 있는 테이블. 인터럽트 종류마다 번호가 있고, 그 번호에 따라 처리해야 할 코드의 위치를 포인터로 가리키고 있는 자료 구조. 해당 인터럽트의 처리 루틴 주소를 가지고 있다.
  • Mode bit: 하드웨어의 보안을 위한 장치. 사용자 프로그램의 잘못된 수행으로 다른 프로그램 및 운영체제에 피해가 가지 않도록 하기 위한 보호장치. Mode bit을 통해 하드웨어적으로 두 가지 모드의 operation 지원

    1 사용자 모드 : 사용자 프로그램 수행
    0 모니터 모드 : OS 코드 수행 (=커널 코드, 시스템 모드)

    • 보안을 해 칠 수 있는 중요한 명령어는 모니터 모드에서만 수행가능한 특권명령으로 규정된다.
    • 인터럽트나 exception 발생 시 하드웨어가 Mode bit을 0으로 바꾼다.
    • 사용자 프로그램에게 CPU를 넘기기 전에 Mode bit을 1로 세팅한다.
  • I/O Device Controller : 해당 I/O 장치 유형을 관리하는 일종의 작은 CPU.

    • 제어 정보를 위해 control register, status register를 가진다.
    • I/O는 실제 device와 local buffer 사이에서 일어난다.
    • Device controller는 I/O가 끝났을 경우 인터럽트로 CPU에 그 사실을 알린다.
  • Device driver (장치구동기): OS 코드 중 각 장치별 처리 루틴 -> software
  • Device Controller: 각 장치를 통제하는 일종의 작은 CPU -> hardware
  • Timer: 정해진 시간이 흐른 뒤 운영체제에게 제어권이 넘어가도록 인터럽트를 발생시킨다.
    • 타이머는 매 클릭 틱 때마다 1씩 감소한다.
    • 타이머 값이 0이 되면 타이머 인터럽트가 발생한다.
    • CPU를 특정 프로그램이 독점하는 것으로부터 보호한다.
    • Time sharing을 구현하기 위해 널리 이용된다.
    • 현재 시간을 계산하기 위해서도 사용된다.
  • Register: 메모리보다 더 빠르면서 정보를 저장할 수 있는 공간. 두 개의 레지스터를 사용하여 프로그램이 접근하려는 메모리 부분이 합법적인지 체크하여 메모리 보호가 이루어진다.
    • 기준 레지스터(base register): 어떤 프로그램이 수행되는 동안 그 프로그램이 합법적으로 접근할 수 있는 메모리 상의 가장 작은 주소를 보관.
    • 한계 레지스터(limit register): 그 프로그램이 기준 레지스터 값부터 접근할 수 있는 메모리의 범위를 보관.
    • 사용자 프로그램은 기준 레지스터 + 한계 레지스터 사이의 주소 영역만 접근 가능하고, 접근하려는 주소가 이 범위 안에 없으면 예외 상황이라는 소프트웨어 인터럽트를 발생시켜 CPU의 제어권을 빼앗는다.
  • 시스템 콜: 입출력 명령은 운영 체제 코드에 구현되어 있어서 사용자 프로그램은 직접 입출력을 수행하는 대신 운영 체제에게 시스템 콜이라는 서비스 대행 요청을 하여 입출력을 수행한다.

인터럽트 수행 순서

  1. 이벤트가 발생하여 컨트롤러가 인터럽트를 발생시킨다. 예를 들어 Disk에서 읽어오라.와 같은 명령을 만나면 CPU가 Disk에 직접 접근하지 않고, OS가 Disk의 Controller에 일을 시키고, 수행한 일은 local buffer에 집어 넣는다. 일이 끝나면 local buffer에 있는 내용을 해당 일을 시킨 프로그램에 copy해준다.
  2. CPU는 하던 일을 멈추고, 인터럽트 벡터를 확인하고, 해당 인터럽트가 있는 위치를 찾아간다.
  3. 인터럽트 서비스 루틴을 통해 해당하는 인터럽트 처리를 완료한다.
  4. 인터럽트의 처리가 완료되었으므로 CPU를 점령당하기 이전의 인터럽트로 돌아가서 하던 작업을 다시 처리한다.

소프트웨어 인터럽트

프로세스가 0으로 나누는 연산 등을 시도하거나 프로그램이 수행되다가 접근해서는 안 되는 메모리 영역에 접근하려 할 때 발생한다. 트랩(Trap)라고도 불린다. 하드웨어 인터럽트처럼 컨트롤러가 발생시키는 것이 아니고, 프로그램 수행 도중 직접 CPU에 인터럽트 라인을 세팅하여 발생시킨다.


입출력 구조

입출력(I/O): 컴퓨터 시스템이 컴퓨터 외부의 주변 장치들과 데이터를 주고 받는 것

입출력 명령어는 다음의 두 가지 방식으로 수행된다.

  • I/O를 수행하는 special instruction에 의해
  • Memory Mapped I/O에 의해

입출력 구조는 동기식 입출력, 비동기식 입출력의 두 가지 방식이 있다. 이 두 가지는 CPU의 제어권 부여 방식에 차이가 있는데, 두 경우 모두 입출력이 완료되었을 때 인터럽트를 통해 알린다.

동기식 입출력

  • 입출력 요청 후 작업이 완료된 후에야 CPU의 제어권이 사용자 프로그램에게 다시 넘어간다.
  • 입출력 연산이 끝날 때까지 CPU는 아무 일도 수행할 수 없다. 이 상태를 봉쇄 상태라고 한다.
  • 입출력 요청의 동기화를 위해 큐(Queue)를 두어 요청 순서대로 처리될 수 있도록 한다. 그 이유는 다수의 입출력이 동시에 요청되거나 처리되는 경우 요청 순서가 뒤바껴 의도치 않은 결과를 일으킬 수 있기 때문이다. 장치마다 큐 헤더가 존재하고 각 장치별로 입출력 수행 순서를 지켜 주기 위한 큐를 관리한다. 컨트롤러는 이 순서에 따라서 매 시점 하니씩 자신에게 주어진 입출력 작업을 처리한다.

비동기식 입출력

  • 입출력 연산을 요청한 후에 연산이 끝나기를 기다리는 것이 아니라 CPU의 제어권을 입출력 연산을 호출한 그 프로그램에게 곧바로 부여한다.
  • 입출력 연산이 완료되는 것과 무관하게 처리가 가능한 작업을 먼저 처리한다.

DMA (Direct Memory Access)

CPU가 주변 장치들의 메모리 접근 요청에 의해 자주 인터럽트 당하는 것을 막아주는 역할

  • 원칙적으로 메모리는 CPU에 의해서만 접근 가능한데 이렇게 될 경우 주변 장치가 메모리 접근을 원할 때마다 인터럽트를 통해 CPU의 업무가 방해받는 비효율이 발생한다.
  • 이러한 CPU 사용의 비효율을 극복하기 위해 CPU 외에 메모리 접근이 가능한 장치인 DMA를 두는 것이다.
  • DMA가 로컬 버퍼에서 메모리로 읽어오는 작업을 대행하기 때문에 CPU에 발생하는 인터럽트 빈도가 줄어든다.
  • DMA는 바이트 단위가 아닌 block 단위로 인터럽트를 발생시킨다.

저장 장치의 구조

컴퓨터의 저장 창치는 주 기억 장치와 보조 기억 장치가 있다.

  • 주 기억 장치(메모리): 전원이 꺼지면 저장되어 있던 내용이 모두 날아가는 휘발성이다.
    • RAM을 매체로 사용하는 경우가 대부분이다.
  • 보조 기억 장치: 전원이 꺼져도 저장된 내용을 기억할 수 있는 비휘발성이다.
    • 마그네틱 디스크, 플래시 메모리, CD, 마그네틱 테이프 등이 사용된다.
    • 보조 기억 장치의 용도
      • 시스템용: 전원이 나가도 유지해야 할 정보는 파일 형태로 저장한다.
      • 스왑 영역용: 메모리의 연장 공간으로 운영 체제가 프로그램 수행에 당장 필요한 부분만 메모리에 올려 두고, 그렇지 않은 부분은 디스크의 스왑 영역에 내려놓게 된다. 그리고 이를 스왑 아웃이라 한다. 하드 디스크가 가장 널리 사용된다.

저장 장치의 계층 구조

빠른 저장 장치일수록 상위, 느린 저장 장치는 하위에 구성되는 계층 구조로 구성된다.

빠른 저장 장치 느린 저장 장치
가격이 높아서 적은 용량을 사용. 가격이 저렴하여 대용량 사용.
속도가 빠르다. 속도가 느리다.
휘발성 비휘발성
register, cache memory, main memory… magnetic disk, optical disk, magnetic tape…
CPU가 직접 접근하여 처리 가능 CPU의 직접 접근 불가

캐슁 기법: 캐쉬 메모리뿐 아니라 상대적으로 용량이 적은 빠른 저장 장치의 성능 향상을 위한 총체적 기법. 상대적으로 느린 저장 장치에 있는 내용 중 당장 사용되거나 빈번하게 사용될 정보를 빠른 저장 장치에 선별적으로 저장해두었다가 두 저장 장치의 속도를 완충시킨다.


프로그램의 구조

컴퓨터 프로그램은 어떠한 프로그래밍 언어로 작성되었든 그 내부적인 구조는 함수들로 구성된다. 그리고 프로그램이 CPU에서 명령을 수행하려면 수행하려는 주소 영역이 메모리에 올라가 있어야 한다. 이 프로그램의 주소 영역은 다음과 같다.

  • 코드: 개발자가 작성한 프로그램 함수들의 코드가 기계어 명령으로 변환되어 저장된다.
  • 데이터: 전역 변수 등 프로그램이 사용하는 데이터를 저장한다.
  • 스택: 함수가 호출될 때, 호출된 함수의 수행을 마치고 복귀할 주소 및 데이터를 임시로 저장한다.

프로그램의 실행

  • 디스크에 존재하던 실행 파일이 메모리에 적재된다는 것을 의미한다.
  • 프로그램이 CPU를 할당받고 기계 명령을 수행하고 있는 상태를 말한다.

사용자 프로그램이 사용하는 함수

  • 사용자 정의 함수: 프로그래머가 직접 작성한 함수
  • 라이브러리 함수: 프로그래머가 직접 작성하진 않았지만 이미 누군가 작성해 놓은 함수를 호출만 하여 사용하는 경우
  • 커널 함수: 운영 체제 커널의 코드에 정의된 함수. 시스템 콜 함수와 인터럽트 처리 함수가 있다.

References
운영체제
[운영 체제와 정보 기술의 원리] 반효경 지음