# 운영체제의 정의 및 목적
- 운영체제는 컴퓨터 시스템에서 하드웨어와 소프트웨어 사이의 인터페이스 역할을 하는 소프트웨어다.
- 자원을 효율적으로 관리하고, 사용자와 시스템 간의 상호 작용을 용이하게 하며, 애플리케이션 실행에 필요한 환경을 제공한다.
- 운영체제의 역사 및 발전 과정
- 1950년대 초반: 최초의 운영체제 개념이 등장, 단일 작업 처리 시스템
- 1960년대: 다중 프로그래밍, 시분할 시스템이 발전, 멀티태스킹이 가능한 시스템 등장
- 1970년대: UNIX 운영체제의 등장, 휴대용 및 다중 사용자 시스템 지원
- 1980년대: 개인용 컴퓨터의 등장, 그래픽 사용자 인터페이스(GUI)가 도입되는 시기
- 1990년대: Windows 95, Linux가 등장, 인터넷이 대중화되며 웹 브라우저가 중요한 역할을 함
- 2000년대 이후: 클라우드 컴퓨팅, 모바일 운영체제, 가상화 기술 등이 발전
- 주요 운영체제의 종류: Windows, macOS, Linux
- Windows: 마이크로소프트가 개발한 운영체제, 개인용 컴퓨터 시장에서 가장 널리 사용됨, 사용자 친화적인 GUI와 넓은 하드웨어 지원
- macOS: 애플이 개발한 운영체제, 애플의 Mac 컴퓨터에 사용됨, 사용자 친화적인 디자인과 효율적인 성능, UNIX 기반
- Linux: 오픈소스 운영체제, 다양한 배포판이 존재, 서버 및 슈퍼컴퓨터, 임베디드 시스템 등에 사용되며, 안정성과 유연성이 특징
## 운영체제의 목적
- 자원관리
- 컴퓨터의 자원인 CPU, 메모리, 디스크 등을 관리하고, 여러 프로세스 또는 응용프로그램 간의 자원 공유 및 충돌을 막는다
- ex) CPU 스케줄링, 메모리 관리, 입출력장치 관리 등.
- 프로세스 관리
- 운영체제는 다수의 프로세스가 동시에 실행될 수 있도록 관리하며, 프로세스의 생성, 제거, 일시정지, 재개 등을 수행한다.
- 프로세스 간의 통신 및 동기화를 위한 기능도 제공한다.
- 자원 보호 및 보안
- 사용자 및 프로세스의 권한을 관리하고, 외부 침입을 막기 위한 방화벽 등을 제공한다.
- 파일의 생성, 삭제, 수정 등을 관리하고, 파일에 대한 접근 권한을 관리하여 프로그램이나 다른 사용자가 데이터를 삭제하거나 중요 파일에 접근하지 못하게 컴퓨터 자원들 보호한다
- 또한 디스크 공간의 할당과 사용 등을 관리한다.
- 인터페이스 제공
- 하드웨어 인터페이스와 사용자 인터페이스 제공하여 편리하게 사용하도록 지원한다

---
## 운영체제의 유형
- 단일 작업 처리 시스템(Single-Tasking System): 하나의 프로그램만 실행 가능한 운영체제.
- 다중 작업 처리 시스템(Multi-Tasking System): 여러 개의 프로그램을 동시에 실행 가능한 운영체제.
- 다중 사용자 처리 시스템(Multi-User System): 여러 사용자가 동시에 시스템을 사용 가능한 운영체제.
- 실시간 처리 시스템(Real-Time System): 일정한 시간 내에 정확한 결과를 출력해야 하는 시스템.
- 분산 처리 시스템(Distributed System): 여러 대의 컴퓨터가 연결되어 하나의 시스템처럼 동작하는 시스템.
### 프로세서의 발전과 운영체제 처리과정의 진화
1. 단일 코어 프로세서
- 초기의 CPU는 단일 코어 프로세서로, 한 번에 하나의 명령어만 처리할 수 있었다.
- 이 때문에 초기 운영체제는 단일 작업 운영체제였으며, CPU는 한 번에 하나의 작업만 처리할 수 있었다.
2. 멀티 코어 프로세서
- 멀티 코어 프로세서가 등장하면서, CPU는 여러 개의 코어를 가지고 병렬로 명령어를 처리할 수 있게 되었다.
- 이로 인해 다중 작업 운영체제에서 여러 개의 프로세스를 동시에 처리할 수 있게 되었으며, CPU는 여러 개의 프로세스를 동시에 처리할 수 있게 되었다.
3. 하이퍼스레딩(Hyper-threading)
- 하이퍼스레딩은 하나의 코어를 여러 개의 가상 코어로 인식하여 병렬처리를 수행할 수 있게 하는 기술이다.
- 이로 인해 CPU는 더욱 병렬처리를 수행할 수 있게 되어, 다중 작업 운영체제에서 더욱 많은 프로세스를 동시에 처리할 수 있게 되었다.
---
# 프로세스
**컴퓨터에서 실행 중인 프로그램**

- 메모리에 올라와 실행되고 있는 프로그램의 인스턴스(독립적인 개체)
- 프로세스는 각각 독립된 메모리 영역(Code, Data, Stack, Heap의 구조)을 할당받는다.
- 기본적으로 프로세스당 최소 1개의 스레드(메인 스레드)를 가지고 있다.
- 각 프로세스는 별도의 주소 공간에서 실행되며, 한 프로세스는 다른 프로세스의 변수나 자료구조에 접근할 수 없다.
- 한 프로세스가 다른 프로세스의 자원에 접근하려면 프로세스 간의 통신(IPC, inter-process communication)을 사용해야 한다.
- 운영체제는 컴퓨터의 자원과 하드웨어를 효율적으로 관리하기 위해 여러 개의 프로세스를 동시에 실행할 수 있다.
## 프로세스의 상태

- 프로세스 : 실행 중인 프로그램으로, 메모리에 로드되어 CPU 자원을 할당받아 작업을 수행하는 단위.
- 프로세스의 상태 : 프로세스가 생성되어 완료될 때까지 거치는 여러 단계
- 새로운 상태(New): 프로세스가 생성되고 있을 때의 상태. 이 상태에서 프로세스는 메모리 할당을 위한 요청을 기다리고 있다.
- 준비 상태(Ready): 프로세스가 메모리에 할당되고, CPU 할당을 기다리는 상태. 준비 상태의 프로세스들은 준비 큐(Ready Queue)에 위치하며, 스케줄러에 의해 실행 상태로 전환될 때를 기다린다.
- 실행 상태(Running): 프로세스가 CPU를 할당받아 실행 중인 상태. 각 프로세스는 CPU를 할당받아 명령어를 실행하고, 작업을 완료하거나 대기 상태로 전환된다.
- 대기 상태(Waiting): 프로세스가 I/O 작업이나 다른 프로세스의 완료 등 특정 이벤트를 기다리는 상태. 이벤트가 발생하면 프로세스는 다시 준비 상태로 전환되어 CPU 할당을 기다린다.
- 완료 상태(Terminated): 프로세스가 작업을 모두 완료한 상태. 이 상태에서 프로세스는 메모리에서 해제되고, 관련 자원들이 반환된다.
## 프로세스 스케줄링 기법
- FCFS(First-Come, First-Served): 먼저 도착한 프로세스가 먼저 처리된다. 공평하지만 평균 대기 시간이 길 수 있다.
- SJF(Shortest Job First): 가장 짧은 실행 시간을 가진 프로세스가 먼저 처리된다. 평균 대기 시간을 최소화하지만, 긴 작업이 계속 대기할 수 있는 기아 현상이 발생할 수 있다.
- Priority Scheduling: 각 프로세스에 우선순위를 부여하고, 높은 우선순위의 프로세스가 먼저 처리된다. 중요한 작업을 먼저 처리할 수 있지만, 낮은 우선순위의 작업은 기아 현상이 발생할 수 있다.
- Round Robin: 각 프로세스에 동일한 시간 할당량(time quantum)을 부여하고, 순환하며 처리한다. 공평한 스케줄링이 가능하지만, 시간 할당량 설정에 따라 성능이 달라질 수 있다.
> 현재 일반적인 OS는 다중 프로세스 및 다중 스레드를 지원하는 멀티태스킹 운영체제로, 여러 개의 프로세스 및 스레드를 동시에 실행한다. 그리고일반적으로 우선순위 기반 스케줄링과 Round-Robin 스케줄링이 결합된 형태인 다중 큐 스케줄링이 사용된다.
---
# 스레드
**프로세스 내에서 실행되는 작은 단위**

- 프로세스의 코드, 데이터 및 자원을 공유하면서 독립적으로 실행되는 일종의 **경량 프로세스**
- 스레드는 프로세스 내에서 각각 Stack만 따로 할당받고 **Code, Data, Heap 영역은 공유한다**
- 스레드는 한 프로세스 내에서 동작되는 여러 실행의 흐름으로, 프로세스 내의 주소 공간이나 자원들(힙 공간 등)을 같은 프로세스 내에 스레드끼리 공유하면서 실행된다.
- 같은 프로세스 안에 있는 여러 스레드들은 같은 힙 공간을 공유한다. 반면에 프로세스는 다른 프로세스의 메모리에 직접 접근할 수 없다.
- 각각의 스레드는 별도의 레지스터와 스택을 갖고 있지만, 힙 메모리는 서로 읽고 쓸 수 있다.
- 한 스레드가 프로세스 자원을 변경하면, 다른 이웃 스레드(sibling thread)도 그 변경 결과를 즉시 볼 수 있다.
> **프로세스와 스레드**
>
> 
>
> ---
>
> 
>
> ---
>
> 
---
## 자바에서의 스레드
- 자바에서 스레드를 생성하고 실행하는 방법은 Thread 클래스를 상속받거나 Runnable 인터페이스를 구현하는 방법이 있다.
**Thread 클래스를 상속받는 방법**
``` java
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread : 스레드가 실행됩니다.");
}
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
```
---
``` java
public class MyRunnable implements Runnable {
public void run() {
System.out.println("MyRunnable : 스레드가 실행됩니다.");
}
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
```
---
## 멀티 스레드

- 스레드는 각자 별도의 레지스터 집합(예: 프로그램 카운터, 스택 포인터 등)과 스택을 가지지만, 힙 영역, 코드 영역, 데이터 영역 등은 다른 스레드와 공유한다.
- 멀티스레드의 개념: 하나의 프로세스 내에서 여러 개의 실행 흐름(스레드)을 가지는 것으로, 스레드들은 동시에 실행되며 프로세스의 메모리 공간을 공유한다.
- 스레드와 프로세스의 차이점: 스레드는 경량 프로세스로, 프로세스보다 메모리와 자원을 덜 소모한다. 프로세스는 독립적인 메모리 공간을 가지지만, 스레드는 동일한 프로세스 내에서 메모리 공간을 공유한다.
- 스레드 동기화 기법: 공유 자원에 대한 동시 접근 문제를 해결하기 위해 세마포어, 뮤텍스, 모니터, 스핀락 등의 동기화 기법이 사용된다.
---
# 컨텍스트 스위칭

**운영 체제에서 실행 중인 프로세스나 스레드를 일시 중지하고, 다른 프로세스나 스레드를 실행하는 것**
- 멀티태스킹(Multitasking)을 가능하게 하기 위한 목적으로, 여러 개의 프로세스나 스레드를 동시에 실행할 수 있도록 한다
- 컨텍스트 스위칭이 발생하는 조건
1. 다른 프로세스나 스레드를 실행시키기 위해 CPU 자원이 필요한 경우
2. 현재 실행 중인 프로세스나 스레드가 입출력 작업 등의 블로킹 상태에 빠진 경우
3. 타이머 인터럽트 등의 이벤트가 발생하여 실행 중인 프로세스나 스레드를 일시 중지해야 하는 경우
---
# 멀티 스레드의 문제
**멀티 스레드 작업을 하게 되면, 경쟁 상태(race condition)문제와 교착상태(deadlock) 문제가 발생할 수 있다**
## 경쟁 상태(race condition)
- 멀티 스레드 환경에서 공유 자원에 접근하는 코드 영역을 임계 구역(Critical Section)이라 한다.
- 여러 스레드가 동시에 접근하는 임계구역에 진입하게 되면 예상치 못한 결과가 발생할 수 있다.
- 예를 들어, 여러 스레드가 동시에 같은 변수를 수정하는 경우, 의도치 않은 값이 저장될 수 있다.
- 경쟁 상태는 여러 스레드가 공유 자원에 동시에 접근하려고 하는 상태를 의미한다.
- 여러 스레드가 공유자원에 대해 원자성이 보장되지 않을 경우 의도치 않은 값의 변경이 일어날 수 있다.
**원자성(Atomicity)**
- 쪼갤 수 없는 연산을 의미
- 아래 두 연산은 원자성을 가지고 있지 않다
- `a = a + 1;`
- `a++;`
- `a = a + 1;`, `a++;` 이 두 코드는 코드상으로는 한 줄에 불과하지만 자바가 컴파일하고 난 뒤로는 여러 연산으로 쪼개진다
- 
- 
> 예시) 여러 스레드가 공유자원 a의 값을 1만큼 증가시키기 과정을 할 때 발생할 수 있는 문제
>
> 
---
경쟁 상태에 대한 문제를 방지하기 위해서, 공유 자원에 대한 접근을 동기화하는 방법이 사용된다.
대표적으로 뮤텍스(Mutex), 세마포어(Semaphore), 모니터(Monitor) 등의 기술이 있다.
## 뮤텍스
- 공유 자원에 대한 접근을 동기화하기 위한 가장 간단한 방법
- 하나의 스레드만이 공유 자원에 접근할 수 있도록 제한하는 동기화 기술
- 뮤텍스를 통해 공유 자원에 대한 동기화를 구현할 때는, 뮤텍스를 획득한 스레드만이 공유 자원을 사용할 수 있고, 뮤텍스를 해제해야 다른 스레드가 사용할 수 있도록 동기화 문제를 해결한다
## 세마포어
- 뮤텍스와 비슷하지만, 여러 개의 스레드가 동시에 접근 가능한 상황을 지원한다
- 정수형 변수를 사용하여 공유 자원에 대한 접근을 제한하는 동기화 기술
- 세마포어 값을 먼저 확인하고, 접근 가능한 경우에만 공유 자원을 사용하도록 함으로써 동기화 문제를 해결한다
## 모니터
- 뮤텍스와 세마포어를 기반으로 한 동기화 기술
- 하나의 프로세스 내에 다른 스레드 간 동기화에 사용된다
- 공유 자원에 대한 동기화를 구현할 때는, 공유 자원에 접근하기 전에 모니터를 획득하고, 작업이 끝나면 모니터를 해제함으로써 동기화 문제를 해결한다
- 자바에서 모니터를 사용하려면 `synchronized` 키워드를 사용하면 된다
### 자바의 `synchronized`
- 자바의 `synchronized` 키워드는 멀티스레드 환경에서 공유 자원에 대한 동시 접근을 제어하기 위한 동기화 메커니즘을 제공한다.
- `synchronized`를 사용하면 한 스레드가 공유 자원에 접근하는 동안 다른 스레드는 해당 자원에 접근할 수 없다.
- 이를 통해 경쟁 조건(race condition)과 같은 문제를 예방할 수 있다.
- `synchronized` 키워드는 다음 두 가지 방식으로 사용할 수 있다.
1. 동기화 메서드
2. 동기화 블록
#### 동기화 메서드
- 메서드 선언부에 synchronized 키워드를 추가함으로써 해당 메서드를 동기화하는 방법
- 이 경우, 해당 메서드에 대한 동기화는 객체 인스턴스에 대해 수행된다.
- 따라서 한 스레드가 동기화된 메서드를 실행하는 동안 해당 객체 인스턴스의 다른 동기화된 메서드에는 다른 스레드가 접근할 수 없다
``` java
public synchronized void myMethod() {
// 동기화된 코드 블록
}
```
#### 동기화 블록
- `synchronized` 키워드를 사용하여 특정 코드 블록만 동기화할 수도 있다.
- 동기화 블록을 사용할 때는 동기화를 수행할 객체를 명시해야 한다.
- 한 스레드가 동기화된 블록에 접근하면, 명시된 객체에 대한 잠금이 이루어지며, 다른 스레드는 해당 객체의 다른 동기화된 블록에 접근할 수 없다.
``` java
synchronized (myObject) {
// 동기화된 코드 블록
}
```
---
## 교착상태(deadlock)

- 상호 배제에 의해 나타나는 문제점으로, 둘 이상의 프로세스들이 자원을 점유한 상태에서 서로 다른 프로세스가 점유하고 있는 자원을 요구하며 무한정 기다리는 현상
## 교착상태의 발생 필요 충분 조건

- 상호 배제(Mutual exclusion): 자원은 한 번에 하나의 프로세스만이 사용할 수 있다.
- 점유 대기(Hold and wait): 프로세스는 하나 이상의 자원을 점유하고 있으면서 그와 동시에 다른 프로세스에서 점유 중인 자원을 추가로 점유하기 위해 대기하고 있어야 한다.
- 비선점(No preemption): 어떤 프로세스가 사용 중인 자원을 다른 프로세스가 요구하면 해당 프로세스는 자원의 사용이 끝날 때까지 기다려야 하며 강제로 빼앗을 수 없다.
- 순환 대기(Circular wait): 각 프로세스는 순환적으로 다음 프로세스가 요구하는 자원을 가지고 있다.
## 교착상태 해결방법
1. 예방(Prevention) : 교착 상태를 예방하기 위해, 상호 배제, 점유 대기, 비선점, 순환 대기 중 하나 이상의 조건을 제거하는 방법. 예를 들어, 모든 자원을 고정된 순서로 요청하도록 강제하는 방법이나, 자원 할당을 하기 전에 요청한 자원이 이미 점유 중인지 미리 검사하는 방법이 있다.
2. 회피(Avoidance) : 교착 상태를 회피하기 위해, 자원 요청에 대한 정보를 바탕으로 교착 상태를 예측하고, 예측된 상황을 피하는 방법. 이를 위해, 자원 요청에 대한 정보와 현재 자원 상태를 고려하여 안전한 요청만을 수락하고, 위험한 요청은 거부한다.
3. 발견(Detection) : 교착 상태를 발견하기 위해, 시스템 상태를 주기적으로 검사하고, 교착 상태를 감지하는 방법. 이를 위해, 자원 할당 그래프를 생성하여 교착 상태 여부를 검사하거나, 자원 요청 시간과 자원 사용 시간을 기록하여 교착 상태를 예측한다.
4. 복구(Recovery) : 교착 상태를 해결하기 위해, 발견된 교착 상태를 해제하는 방법. 이를 위해, 교착 상태를 발견하면, 자원을 선점하여 다른 스레드에게 할당하거나, 자원을 반납하여 다른 스레드가 사용할 수 있도록 한다.
### 식사하는 철학자들
> 컴퓨터 과학에서 멀티스레딩과 동기화 문제를 설명하기 위한 고전적인 예시로, 다수의 프로세스나 스레드가 공유 자원에 접근할 때 발생할 수 있는 교착 상태(deadlock), 병목 현상, 비동기 접근에 대한 이슈 등을 설명하기 위해 사용된다

1. 원형 테이블에 철학자들이 앉아있다.
2. 각 철학자 사이에는 젓가락이 하나씩 놓여 있으며, 총 N개의 젓가락이 있다(N명의 철학자가 있다고 가정).
3. 철학자들은 생각하거나 식사를 할 수 있다.
4. 식사를 할 때, 철학자는 자신의 양 옆에 있는 두 개의 젓가락을 사용해야 한다.
5. 철학자는 생각한 뒤 식사를 하고, 식사를 마친 후에는 다시 생각한다.
이러한 상황에서 다음과 같은 문제가 발생할 수 있다.
- 교착 상태(Deadlock): 모든 철학자가 동시에 자신의 왼쪽 젓가락을 집으면, 오른쪽 젓가락을 집을 수 없어 모두 교착 상태에 빠진다.
- 기아 상태(Starvation): 한 철학자가 계속해서 젓가락을 얻지 못하고 기다리는 상황이 발생할 수 있다.
- 불공평한 자원 할당(Unfairness): 한 철학자가 자주 식사하는 반면 다른 철학자는 자주 기아 상태에 빠질 수 있다.
> 이 문제를 해결하기 위한 다양한 알고리즘이 제시되어 있으며, 그 중 하나는 철학자들이 동시에 왼쪽 젓가락을 집어도록 하는 것이 아니라 홀수 번째 철학자는 왼쪽 젓가락부터, 짝수 번째 철학자는 오른쪽 젓가락부터 집는 방식이다. 이렇게 하면 동시에 모든 철학자가 젓가락을 집을 수 없으므로 교착 상태를 피할 수 있다.
---
## 과제 1
목표 : `MyList<T>`라는 이름의 인터페이스를 만들어 이전 시간에 작성한 `MyArrayList`와 `MyLinkedList` 두 클래스를 추상화한다.
**요구사항**
- `MyList<T>`는 다음과 같은 추상 메서드를 가지고 있다.
- `void add(T data)`: 리스트의 끝에 새로운 요소를 추가한다.
- `void add(int index, T data)`: 지정된 인덱스에 새로운 요소를 삽입한다.
- `T remove(int index)`: 지정된 인덱스의 요소를 삭제하고, 삭제된 요소의 데이터를 반환한다.
- `T get(int index)`: 지정된 인덱스의 요소 데이터를 반환한다.
- `int size()`: 리스트의 크기(요소 수)를 반환한다.
- `boolean isEmpty()`: 리스트가 비어 있는지 확인한다.
- 위 요구사항을 만족하는 `MyList` 인터페이스를 구현하고, `MyArrayList`와 `MyLinkedList` 두 클래스가 이 인터페이스를 상속받아 동작했을 때 문제 없이 돌아가도록한다.
---
## 과제 2
과제: 은행 계좌를 나타내는 `BankAccount` 클래스를 작성해보면서, `synchronized` 블럭으로 동기화 하는 방법을 배워본다
**요구사항**
1. `BankAccount` 클래스를 생성. 이 클래스는 아래의 메서드를 포함해야 한다.
- `public void deposit(double amount)` : 계좌에 금액을 입금하는 메서드. `synchronized` 키워드를 사용하여 동시에 여러 스레드에서 입금이 발생해도 정확한 잔액이 유지되도록 한다.
- `public void withdraw(double amount)` : 계좌에서 금액을 출금하는 메서드. `synchronized` 키워드를 사용하여 동시에 여러 스레드에서 출금이 발생해도 정확한 잔액이 유지되도록 한다.
- `public double getBalance()` : 계좌의 현재 잔액을 반환하는 메서드.
2. 여러 개의 스레드를 생성하여 `BankAccount`의 입금 및 출금 메서드를 동시에 호출해본다. 스레드가 모두 실행된 후에 잔액이 올바르게 계산되었는지 확인한다.
3. 과제를 완료한 후에는 여러 스레드에서 동시에 작업을 수행해도 `synchronized` 키워드 덕분에 잔액이 정확하게 유지되는 것을 확인할 수 있다. 이를 확인하기 위해, 각 스레드에서 입금 및 출금 작업을 수행한 후 최종 잔액이 올바른지 검증해본다. 예를 들어, 5개의 스레드가 각각 100달러를 입금하고 50달러를 출금한다면, 최종 잔액은 250달러가 되어야 한다.
---
## 과제 3
목표: 자바로 스택을 구현한다.
**요구사항**
- 제네릭 클래스를 사용하여 다양한 데이터 타입을 저장할 수 있는 스택(`MyStack`) 클래스를 구현한다.
- `MyStack` 클래스는 다음의 메소드를 포함해야 한다
- `public void push(T data)`: 스택의 맨 위에 새로운 요소를 추가한다.
- `public T pop()`: 스택의 맨 위에 있는 요소를 제거하고 반환한다. 스택이 비어있는 경우, 예외를 발생시킨다.
- `public T peek()`: 스택의 맨 위에 있는 요소를 반환한다. 스택이 비어있는 경우, 예외를 발생시킨다.
- `public int size()`: 스택의 크기(요소 수)를 반환한다.
- `public boolean isEmpty()`: 스택이 비어 있는지 확인한다.
**참고 사항**
- 스택의 내부 구현은 배열 또는 연결 리스트를 사용할 수 있다.
- 작성한 `MyStack` 클래스를 사용하여 다양한 데이터 타입의 요소를 `push`, `pop`, `peek` 할 수 있는 예제도 같이 작성한다.
---
## 과제 4
목표: 자바로 제네릭을 사용한 큐(MyQueue) 클래스를 구현한다.
**요구사항**
- 제네릭 클래스를 사용하여 다양한 데이터 타입의 요소를 저장할 수 있는 큐(`MyQueue`) 클래스를 구현한다.
- `MyQueue` 클래스는 다음의 메소드를 포함해야 한다
- `public void enqueue(T data)`: 큐의 끝에 새로운 요소를 추가한다.
- `public T dequeue()`: 큐의 맨 앞 요소를 제거하고 반환한다.
- `public T peek()`: 큐의 맨 앞 요소 데이터를 반환한다 (제거하지 않음).
- `public int size()`: 큐의 크기(요소 수)를 반환한다.
- `public boolean isEmpty()`: 큐가 비어 있는지 확인한다.
###### tags: `과외(하희영)`