728x90
반응형

문제 해설

이 문제는 a는 S에서 출발하여 A까지 가야하고, b는 S에서 출발하여 B까지 도착해야 한다. 그리고 만약 같이 합승한다면 돈은 한번만 내도 된다.
처음에는 진짜 너무 복잡하게 생각했었는데, S->A로 가는 모든 경로를 구하고 S->B로 가는 모든 경로를 구한다음.. 서로 조합을 해서 같은 경로를 구하고.. 같으면 비용 안내도 되니까 한번만 계산해서 그 중 최솟값을 구할려고 했다...

이걸 굳이 각각 나누어서 경로를 구할 필요가 없었다. 결국에는 어떤 노드까지는 a와 b가 같이 가고, 그 다음 그 노드에서 a는 A까지, b는 B까지 가면 된다. 만약 x->y로 가는 최소비용을 알고 있으면 쉬워진다. 그럼 1부터 n까지의 노드들을 다 탐색하여 같이 가는 노드를 구한 다음, a,b 따로 가는 비용을 구해주면 된다.

여기서 x->y로 가는 최소비용을 구하는 방법은 2가지이다. 다익스트라를 이용해서 구할 수 있고, 모든 비용을 구해야하기 때문에 플로이드 워셜을 사용해도 된다.

다익스트라를 이용한 방법

플로이드 워셜보다 다익스트라를 먼저 생각한 것은 시간 효율성 때문이다. 다익스트라는 시간복잡도가 O(ElogV)이기 때문에 여기서 n은 200이하의 자연수이기 때문에 O(E)가 나온다. 즉, 간선의 개수와 시간복잡도가 같다. (O(Elog200) -> O(2E) -> O(E))

파이썬

import heapq

def solution(n, s, a, b, fares):
    INF = int(1e9)
    answer = INF

    def dijkstra(start):
        dist = [INF for _ in range(n + 1)]
        dist[start] = 0
        q = []
        heapq.heappush(q, [dist[start], start])
        while q:
            cur_dist, node = heapq.heappop(q)
            for tmp_node, tmp_dist in cost[node]:
                if dist[tmp_node] > cur_dist + tmp_dist:
                    dist[tmp_node] = cur_dist + tmp_dist
                    heapq.heappush(q, [dist[tmp_node], tmp_node])
        return dist

    cost = [[] for _ in range(n + 1)]
    for start, end, val in fares:
        cost[start].append((end, val))
        cost[end].append((start, val))

    dist = []
    for i in range(n + 1):
        dist.append(dijkstra(i))
    # print(dist)
    for i in range(1, n+1):
        answer = min(dist[s][i] + dist[i][a] + dist[i][b], answer)
    return answer
import java.util.ArrayList;
import java.util.Arrays;
import java.util.PriorityQueue;

class Solution {
    class Node implements Comparable<Node> {
        int node;
        int distance;

        public Node(int node, int distance) {
            this.node = node;
            this.distance = distance;
        }

        @Override
        public int compareTo(Node o) {
            return Integer.compare(this.distance, o.distance);
        }
    }

    public int solution(int n, int s, int a, int b, int[][] fares) {
        int answer = Integer.MAX_VALUE;

        ArrayList<Node>[] cost = new ArrayList[n + 1];
        for (int i = 1; i <= n; i++) {
            cost[i] = new ArrayList<>();
        }

        for (int[] fare : fares) {
            int start = fare[0];
            int end = fare[1];
            int val = fare[2];
            cost[start].add(new Node(end, val));
            cost[end].add(new Node(start, val));
        }

        int[][] dist = new int[n + 1][n + 1];
        for (int i = 1; i <= n; i++) {
            Arrays.fill(dist[i], Integer.MAX_VALUE);
        }

        for (int i = 1; i <= n; i++) {
            dijkstra(i, n, cost, dist[i]);
        }

        for (int i = 1; i <= n; i++) {
            answer = Math.min(answer, dist[s][i] + dist[i][a] + dist[i][b]);
        }

        return answer;
    }

    public void dijkstra(int start, int n, ArrayList<Node>[] cost, int[] dist) {
        PriorityQueue<Node> pq = new PriorityQueue<>();
        pq.add(new Node(start, 0));
        dist[start] = 0;

        while (!pq.isEmpty()) {
            Node cur = pq.poll();

            for (Node next : cost[cur.node]) {
                int newDist = dist[cur.node] + next.distance;
                if (newDist < dist[next.node]) {
                    dist[next.node] = newDist;
                    pq.add(new Node(next.node, newDist));
                }
            }
        }
    }
}

플로이드-워셜을 이용한 방법

플로이드 워셜은 노드들을 거치는 최단거리를 모두 다 구하게 된다. 그래서 노드들을 3번 탐색하게 되어 시간복잡도가 O(N^3)이 나오게 된다.
이 방법은 시간초과를 항상 유념해야 하는데, 문제조건에서 N은 200이하이기 때문에 O(10^6)정도가 나오게 되어 가능하다.

파이썬

def solution(n, s, a, b, fares):
    INF = int(1e9)
    cost = [[INF for _ in range(n + 1)] for _ in range(n + 1)]
    for i in range(1, n + 1):
        cost[i][i] = 0
    for fare in fares:
        start = fare[0]
        end = fare[1]
        val = fare[2]
        cost[start][end] = val
        cost[end][start] = val
    for k in range(1, n + 1):
        for i in range(1, n + 1):
            for j in range(1, n + 1):
                cost[i][j] = min(cost[i][j], cost[i][k] + cost[k][j])
    answer = INF
    for i in range(1, n + 1):
        answer = min(answer, cost[s][i] + cost[i][a] + cost[i][b])
    return answer

자바

import java.util.Arrays;
class Solution {
    static int[][] cost;
    public int solution(int n, int s, int a, int b, int[][] fares) {
        cost = new int[n + 1][n + 1];

        for (int i = 1; i <= n; i++) {
            Arrays.fill(cost[i], 1000000);
        }

        for (int[] fare : fares) {
            int start = fare[0];
            int end = fare[1];
            int val = fare[2];
            cost[start][end] = val;
            cost[end][start] = val;
        }

        for (int k = 1; k <= n; k++) {
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= n; j++) {
                    if (i != j) {
                        cost[i][j] = Math.min(cost[i][j], cost[i][k] + cost[k][j]);
                    }
                }
            }
        }
        for (int i = 1; i <= n; i++) {
            cost[i][i] = 0;
        }

        int answer = Integer.MAX_VALUE;
        for (int i = 1; i <= n; i++) {
            answer = Math.min(answer, cost[s][i] + cost[i][a] + cost[i][b]);
        }

        return answer;
    }
}

정답 및 후기

python은 두 가지 방법 모두 통과하였지만, java의 경우 다익스트라에서 효율성에서 4개를 틀렸었다.
원인은 시간복잡도 때문인데, 시간초과가 나는 이유는 다익스트라에서 간선이 많은 경우에 문제가 생기는 것 같다.
간선이 많으면 위의 문제에서 시간복잡도가 초과될 수 있다.

항상 음수간선이 없으면 플로이드-워셜보다 다익스트라를 먼저 생각했는데(일반적으로 간선보다 노드 개수에 제한이 많은 경우가 많아서..)
이 경우는 노드가 200이하라 플로이드-워셜이 더 효과적인 문제였다.

728x90
반응형

'알고리즘 > 알고리즘 문제' 카테고리의 다른 글

[알고리즘] 2048 (Easy)  (0) 2023.10.08
[알고리즘] 친구비  (2) 2023.06.03
[백준] 타임머신 11657  (0) 2023.03.22
[백준 2636] 치즈  (0) 2023.03.11
[백준 - 1062] [파이썬] 가르침  (0) 2023.01.08
728x90
반응형

웹은 기본적으로 클라이언트가 요청하면 서버에서 데이터를 내려주는 식으로 동작한다. 여러 방식에 따라 어떤 언어와 프레임워크를 선택할지 다르다.
클라이언트(브라우저)가 서버에서 html, css, js 모든 것을 받아서 보여줄 수도 있고, html, css를 서버에서 받은 js를 이용하여 보여줄 수도 있다.
그리고 동적인 정보, 정적인 정보 어떤 정보를 받느냐에 따라서 달라질 수 있다.

간단한 웹 서비스를 개발하려면 동적인 것이 필요없을 수 있다. 그냥 html, css, js를 서버로부터 받아와서 보여주기만 하면 되고, 클라이언트가 클릭하거나 하는 동적인 활동이 필요없는 웹 화면일 수 있다. 하지만 웹 서비스에 동적인 리소스가 필요없을 수가 없다.

처음에는 WAS 하나로 모든 것을 해결해주었다. 서버에서 정적인 파일인든, 동적인 파일이든 애플리케이션 로직과 분리도 되어있지 않고 모든 것을 포함하는 것이다. 그치만 이렇게 하면 WAS가 너무 많은 역할을 담당하게 된다. 그래서 WAS를 나눈다.

웹 애플리케이션 서버(WAS)

WAS는 제일 큰 개념이라고 생각하면 된다.

WAS는 Web Server랑 서블릿 컨테이너 그리고 데이터베이스 연결 등등이 있지만, Web Server와 서블릿 컨테이너를 위주로 보겠다.

Web Server

웹 서버를 정적인 리소스를 이용하여 전달해주는 것이다. 웹 서버의 예시로는 Apache HTTP Server나 NginX같은 것이 있다.

서블릿 컨테이너

서블릿 컨테이너는 서블릿을 실행하기 위한 환경을 제공하고 서블릿의 생명주기를 관리한다. 그럼 서블릿이란 뭘까?

서블릿

이전에 동적인 동작을 처리하기 위해서 서블릿이 필요하다. 서블릿은 클라이언트의 요청을 담당하고 동적인 웹 콘텐츠를 생성하고 전송한다. 서블릿은 Java Servlet API에 정의되어 있는 인터페이스를 구현하여서 개발된다. 즉, WAS는 서블릿 컨테이너를 내장하고 있어서 서블릿을 실행할 수 있게 되는 것이다.

서블릿은 HTTP 요청을 받고 HTTP 응답을 JSON 형식으로 내려줄 수 있다. 서블리의 예시로는 톰캣이나 Jetty가 있다. 여기서 톰캣을 가지게 되는 서블릿 컨테이너의 이름이 Apache Tomcat이다.

Spring, Apache Tomcat

Spring Framework 자체적으로는 Apache Tomcat을 내장하고 있지 않아서 만약 Spring 애플리케이션을 실행하기 위해서는 별도로 Tomcat을 설치하고 설정해야 한다. 이 과정이 복잡하고 어렵기 때문에 Spring Boot를 프로젝트를 시작할 때 사용하게 된다.

Spring Boot 사용 이유

Spring Boot 프로젝트를 사용할 경우에는 내장 서블릿 컨테이너로 Tomcat, Jetty, Undertow 중 하나를 선택하여 사용할 수 있다. Spring Boot는 웹 애플리케이션을 실행하는 데 필요한 내장 서버를 자동으로 제공하므로 별도의 서버 설정이 필요하지 않다.

Spring Framework는 서블릿 컨테이너와 통합되어 웹 애플리케이션 개발을 지원하지만, Spring 자체적으로 Apache Tomcat을 내장하고 있는 것은 아니고, Apache Tomcat은 Spring 애플리케이션을 실행하기 위한 선택적인 서블릿 컨테이너 중 하나이고 Spring Boot에서 자체적으로 제공한다고 생각하면 된다.

728x90
반응형

'백엔드' 카테고리의 다른 글

GithubAction을 이용해서 Spring 프로젝트 CI/CD(with Docker)  (0) 2023.09.06
세션  (0) 2023.08.25
[AWS] Elastic BeanStalk 배포와 Trouble Shooting  (0) 2023.05.23
Gradle  (0) 2022.12.27
Amazon S3  (0) 2022.07.12
728x90
반응형

Java 컬렉션 (Collection) 정리

Java Collection은 Java 언어에서 데이터를 저장, 처리, 관리하는 데 사용되는 API 모음이다.

특히나 자료구조에 대해서 이해한다면 쉽게 이해해서 사용할 수 있다.

객체들을 효율적으로 추가, 삭제, 검색할 수 있도록 java.util 페키지에 collection과 관련된 인터페이스와 클래스를 포함시켰기 때문에 사용할 수 있다.

(매번 찾아보는 것보다 한 번 정리해야겠다는 생각이 들어서 쓰게 되었다.)

데이터 구조엔 여러가지 종류들이 있다 : ArrayList, LinkedList, Hashtable, Tree ...

java가 제공하는 집합 인터페이스 : List, Set, Queue, Map 그리고 각각의 구현체들



List

  • 순서 있는 집합 → 어느 요소가 어디에 삽입되는지 제어가능!
  • 중복된 요소들을 허용
  • 어느 위치에 어떤 객체가 있는지를 신경씀
List<String> words = List.of("Apple", "Bat", "Cat"); // of는 정적 메소드
words.size(); // 3
words.isEmpty(); // false
words.get(0); // Apple
words.contains("Dog"); // false
words.indexOf("Cat"); // 2

of 메소드
: Java 9부터 추가된 List 인터페이스의 메소드. 주어진 요소들을 포함하는 불변리스트를 생성함.
리스트 생성하고 초기화할 때 사용하면 좋다. 그리고 null 요소도 허용하지 않는다.
List.of로 만든 instance는 수정할 수 없다. (불변 리스트)

immutable List
불변 클래스란 무엇일까? -> String, BigDecimal, Wrapper 클래스는 불변이다.
이 의미는 특정 클래스의 인스턴스를 만든 순간부터 이 값을 바꿀 수 없게 된다는 것이다.

가변 리스트를 만들고 싶으면 ArrayList나 LinkedList, Vector를 만들어야 한다.

List<String> wordsArrayList = new ArrayList<String>(words);
List<String> wordsLinkedList = new LinkedList<String>(words);
List<String> wordsVector = new Vector<String>(words);

ArrayList와 LinkedList의 차이

  • ArrayList는 접근은 빠르지만 삽입 삭제가 느리다. 메모리가 연속적
  • LinkedList는 접근은 느리지만 삽입 삭제가 빠르다. 메모리가 따로따로. 서로 연결되는 메모리 주소를 기억하고 있다.
  • java에서의 LinkedList는 요소들이 양면으로 연결되어 있다. forward로 향하는 연결과 backward로 향하는 연결이 둘 다 존재한다. (doubly linked)
  • 변경점이 적다면 ArrayList, 삽입과 제거가 빈번하다면 LinkedList를 사용하면 된다.

Array : 배열은 모든 요소들을 순서대로 저장하고 언제나 요소 하나를 제거하면 그 요소를 제거한 후 그 다음 요소를 왼쪽으로 밀어야 한다. 하지만, 배열의 특정한 위치에 있는 요소를 갖고 오는 건 쉽다.

LinkedList : 각자 요소들은 다음 요소들과 연결되어 있다. 삽입과 삭제가 더 쉽다. 하지만 특정 위치에 있는 요소를 갖고 오거나 찾는 것은 연결점들을 다 건너 다니면 무엇이 있는지 찾아야 한다.


Vector랑 ArrayList는 언제 구분해서 써야할까?

Vecotr 클래스는 자바1때부터 있었다. ArrayList는 자바1.2부터.(오래 전부터 있었다는 뜻)

그럼 Vector의 문제가 무엇이여서 ArrayList가 나타난 것일까?

  • vector는 모든 메서드들이 synchronize가 되어있는데, ArrayList는 Synchronize되어 있지 않다.
  • vector는 내부의 'add', 'remove', 'set'과 같은 변경 작업을 할 때 내부적으로 synchronized를 사용한다.
  • Synchronize 키워드는 어떤 차이를 만들까?
    • 예를 들어서 한 클래스 안에 25개의 동기화된 메소드들이 있다고 하자.
    • 만약 이 Vector 클래스의 인스턴스가 여러 스레드 사이에 공유된다면, 스레드 중 단 하나만이 이 25개의 메소드들을 실행할 수 있다.
    • 즉, 이 동기화된 메소드들 안에서는 한순간에 오직 하나의 스레드만 코드를 실행시킬 수 있다.
    • 이렇게 하는 이유는 프로그램이 안전하길 바라기 때문이다.
    • 우리의 프로그램은 쓰레드 하나가 사용하든 15개의 쓰레드가 사용하든 행동방식이 바뀌면 안되고, Synchronized가 그 역할을 하려 하는 것이다.
    • Vector는 Thread-safe하다. 여러 쓰레드들 사이에서 데이터를 공유하는 상황에서 Vector를 사용할 수 있다.
    • 그러나 ArrayList는 안전하지 않다. 그치만, 보안은 언제나 성능에 타격을 준다.(보안 상승, 성능 하락 / 보안 하락, 성능 향상)
    • 왜냐하면 스레드 하나가 동기화된 메소드를 실행 중일때 그럼 다른 스레드들은 그 스레드가 동기화된 메소드의 실행을 완료할때까지 기다릴 수 있기 때문이다.
    • 안전이 필요하지 않는 이상 ArrayList가 더 낫다.
    • 동기화는 기초적인 안전 구현방식 중 하나이다.
      • 그래서 만약 멀티 쓰레드 환경이 아닐 때 Vector 클래스를 사용하게 되면 성능이 떨어지게 된다.
    • 이를 위해, ArrayList를 위해 Collections 클래스에서는 synchronizedList 메소드를 제공한다.
    • ArrayList클래스를 멀티스레드 환경에서 사용해야 한다면 CopyOnWriteArrayList 또는 Collections.synchronizedList를 사용해야 한다.

반복문

java Collection 인터페이스의 Iterator는 Collection 요소를 하나씩 반복적으로 접근할 수 있다.

  • hasNext() : 다음 요소가 있는지 true, false를 반환함
  • next() : 다음 요소 반환
    for(int i=0;i<words.size();i++){
      System.out.println(words.get(i));
    }
    

for(String word:words){
System.out.println(words.get(i));
}

// Iterator 사용하기
Iterator wordsIterator = words.iterator();
while(wordsIterator.hasNext()){
System.out.println(wordsIterator.next());
}


그럼 3가지 중 어떤 루프를 써야할까?

```java
while(wordsIterator.hasNext()){
    System.out.println(wordsIterator.next());
}
List<String> words = List.of("Apple","Bat","Cat");
List<String> wordsAl = new ArrayList<>(words);
for(String word:words){
    if(word.endsWith("at"))
        System.out.println("word = " + word);
}

밑의 방법은 추천하지 않는다.

for(String word:wordsAl){
    if(word.endsWith("at")){
        wordsAl.remove(word);
    }
}

해당 방법은 개선된 loop의 중간에서 변경점을 만들면 단어를 제거함으로써 반복이 어떻게 진행되는지가 바뀔 수가 있기 때문에, 반복자를 사용하는 것이 좋다.

Iterator<String> iterator = wordsAl.iterator();
while (iterator.hasNext()) {
    if(iterator.next().endsWith("at")){
        iterator.remove();
    }
}

List 안에는 primitive type를 포함할 수 없다. List 인터페이스는 제네릭 타입을 지원하기 때문에, List 내부에는 오직 객체만 포함시킬 수 있다.
그래서 사용하려면 primitive type을 객체로 래핑한 Wrapper 클래스를 사용하여 포함해야 한다.

AutoBoxing : primitive 타입의 값을 해당하는 wrapper 클래스의 객체로 바꾸는 과정을 의미한다.

java 컴파일러는 primitive type이 아래 두 가지 경우에 해당될 때 autoBoxing을 적용한다.

  1. primitive type이 Wrapper 클래스의 타입의 파라미터를 받는 메서드를 통과할 때
  2. primitive type이 Wrapper 클래스의 변수로 할당될 때

List를 만들려 할 때 일어나는 일은 다 AutoBoxing되어 Wrapper Class가 생성되는 것이다.


sort static solution
Collections.sort는 static method이다. (즉, Collection을 따로 생성자를 통해 생성하지 않아도 사용할 수 있다. )

List는 Collection Interface를 연장한다. 즉, 이것은 Collection Interface에 있는 모든 것을 구현하고, 객체의 위치에 상관하는 메서드를 제공한다.


정리 : List interface의 구현

→ ArrayList : 배열을 기초적 데이터 구조로 사용. LinkedList에 비해서 삽입과 삭제가 느리다.

but, 특정 위치의 특정 요소에 접근하고 싶다면 매우 빨리 수행할 수 있다.

→ LinkedList : 특정 위치의 요소 찾는 것은 느리지만, 요소의 삽입과 제거는 훨씬 빠르다

→ Vector : 다중 스레드 시나리오에서는 성능 하락.



Set

  • 용도 : 중복이 허용되지 않는 집합을 만들 때 사용한다.
    Unique things only : 중복이 허용되지 않는다!
  • List interface와 비교했을 때, Set interface는 위치 접근을 허용하지 않는다. Set은 그냥 랜덤위치에 저장한다. (갖고 있다는 사실이 중요)
  • Set은 기본적으로 변경을 허용하지 않는다. 따라서, 다음 코드는 되지 않는다.
Set<String> set = Set.of("Apple","Banana","Cat");
set.add("Apple"); // 오류남. -> 왜냐하면 변경을 허용하지 않기 때문
  • HashSet : Set의 여러 구현 중 하나.
  • 안에 요소들이 있는지 없는지는 신경쓰지만, 위치는 신경쓰지 않는다.
set.add(2,"Apple");
  • 특정 위치에 요소를 추가할 수 없다. Set은 유일한 값들을 저장하는데 사용된다.


Queue

작업하고 싶은 순서대로 정렬할 때 사용된다.

컬렉션의 메서드를 모두 지원한다.

우선순위 Queue : 지정한 순서대로 정렬되어 있다.

물론 Comparator로 순서를 지정할 수 있다.

Queue<String> queue = new PriorityQueue<>();
queue.poll(); // 아무것도 없어서 안빠짐
queue.offer("Apple");
queue.add("Banana");
System.out.println("queue = " + queue); // queue = [Apple, Banana]

queue.addAll(List.of("Zebra", "Sero"));
System.out.println("queue = " + queue); // queue = [Apple, Banana, Zebra, Sero]

System.out.println(queue.poll()); // Apple
System.out.println(queue.poll()); // Banana
System.out.println(queue.poll()); // Sero
System.out.println(queue.poll()); // Zebra
System.out.println(queue.poll()); // null

우선순위 대로 poll해서 뺐을 때, 나오는 걸 볼 수 있다.

Queue<String> queue = new PriorityQueue<>();
queue.addAll(List.of("Zebra", "Monkey", "Cat"));
System.out.println(queue.poll()); // Cat
System.out.println(queue.poll()); // Monkey
System.out.println(queue.poll()); // Zebra

Comparator 지정하기 -> 이 코드에서는 길이가 짧은 순으로 나오게 할 수 있다.

static class StringLengthComparator implements Comparator<String> {

    @Override
    public int compare(String o1, String o2) {
        return Integer.compare(o1.length(), o2.length());
    }
}

public static void main(String[] args) {

    Queue<String> queue = new PriorityQueue<>(new StringLengthComparator());
    queue.addAll(List.of("Zebra", "Monkey", "Cat"));
    System.out.println(queue.poll()); // Cat
    System.out.println(queue.poll()); // Zebra
    System.out.println(queue.poll()); // Monkey
}


Map

→ Map은 Collection interface를 연장하지 않는다! Map은 Collection 프레임워크의 일부이지만 Collection 인터페이스를 구현하지는 않는다!

키-쌍을 저장하는데 사용한다.

HashMap(정렬되지 않고, 순서없음)

  • HashMap과 Hashtable의 차이 : 둘 다 해싱 기법을 사용했다는 점에서는 동일하다Hashtable(HashMap과 같지만, 모든 메서드가 동기화되어 있어 스레드가 안전)
  • Hashtable은 Vector와 비슷하다 → 동기화되어 있음. Hashtable의 모든 메소드는 동기화 되어 있다.
    • 스레드가 HashMap에 비해 더 안전하고, HashMap과 동일하게 Hashtable도 분류되어 있지도 않고 순서가 있지도 않다.
  • Hashmap은 열쇠를 null 값과 저장할 수 있게 해준다. → Hashmap 안에는 key와 null값을 저장할 수 있다. (Hashtable에서는 할 수 없다.)LinkedHashMap(삽입 순서 기억)
  • LinkedHashset과 비슷하게 순서가 유지된다. 그냥 삽입 순서대로 들어간 것이기 때문에 HashMap에 비해서는 삽입과 제거가 느리다. 하지만 요소간에 연결이 되어있어 요소를 도는 이터레이션은 훨씬 빠르다.TreeMap(데이터를 정렬된 순서로 저장)
  • 기반 데이터 구조는 Tree, 정렬된 순서로 저장된다.
  • 트리가 존재할 때는, 데이터가 정렬되어 있기 때문에, 다른 인터페이스(NavigableMap)도 구현한다.

Hashtable

배열과 비슷하게 고정된 위치들과, LinkedList의 장점을 합친 것이다. 각각의 위치를 양동이라고 하면 양동이에 여러 가지를 저장할 수 있는 것이다.
양동이에 차곡차곡 여러 가지를 쌓을 수 있는 것이다.
그럼 어떤 양동이에 저장할지 결정하느냐? 이때 나오는 것이 해싱 함수를 사용한다.
만약 배열의 크기가 13이라고 생각해보자. (index는 그럼 0부터 12까지 있는 것이다.)

만약 15를 어디에 저장해야 할지 결정한다면 어느 양동이에 15를 저장해야 할지 어떻게 정할까? 여기에 13개의 양동이가 있으므로, 15를 13으로 나누고 나머지를 구해서 양동이에 요소를 넣는 것이다.

15는 그럼 인덱스 2의 양동이에 들어가게 되는 것이다.

즉, 해싱 함수는 어느 양동이에 요소가 들어갈지 정해주는데 사용된다.

2를 삽입한다면 2번 자리에 저장하고 싶을 것이니까 아까 저장한 15위에 2를 붙이면 된다.

34를 지우고 싶으면 8인덱스에 와서 요소를 지운다. (34나누기 13의 나머지는 8이니까!)

HashTable의 장점은 요소들을 쉽게 삽입할 수 있고, 검색과 제거 또한 훨씬 쉽게 할 수 있다.

HashTable은 매우 빠른 검색능력을 제공한다. 요소의 삽입은 때때로는 LinkedList보다 느릴 수 있지만, 배열에 비해서는 훨씬 빠르다.
HashTable의 효율성은 언제나 해싱 함수의 효율성에 기반한다.(위에 예시로 든 건 그냥 설명을 위해서이지 해싱함수마다 다르다.)

Java에서는 해싱함수를 해시코드란 것을 이용하여 구현한다. 객체 클래스를 보면, hashcode()라는 메서드가 있다.

hashcode는 어느 양동이에 객체가 저장되는지를 결정하는데 사용된다. 위에서 얘기하였던 나머지를 이용해서 저장하는 것은 그냥 예시일 뿐이다. 해싱 함수들은 hashcode()를 이용해서 java에서 구현할 수
있다.

Map<String, Integer> map = Map.of("A", 3, "B", 5, "z", 10);
// map.put("R", 1); of를 써서 삽입할 수 없음
System.out.println(map.get("z"));
System.out.println(map.size());
System.out.println(map.containsKey("A"));
System.out.println(map.containsValue(3));
System.out.println(map.keySet());
System.out.println(map.values());

/*
10
3
true
true
[z, A, B]
[10, 3, 5]
*/
Map<String, Integer> hashmap = new HashMap<>(map);
hashmap.put("F", 5);
System.out.println(hashmap);
hashmap.put("z", 11);
System.out.println(hashmap);

/*
{A=3, z=10, B=5, F=5}
{A=3, z=11, B=5, F=5}
*/

HashMap, LinkedHashMap, TreeMap 서로 비교

Map<String, Integer> hashmap = new HashMap<>();
hashmap.put("Z", 5);
hashmap.put("A", 15);
hashmap.put("F", 25);
hashmap.put("L", 250);
System.out.println("hashmap = " + hashmap); // hashmap = {A=15, F=25, Z=5, L=250} -> 순서 맘대로

LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("Z", 5);
linkedHashMap.put("A", 15);
linkedHashMap.put("F", 25);
linkedHashMap.put("L", 250);
System.out.println("linkedHashMap = " + linkedHashMap); // linkedHashMap = {Z=5, A=15, F=25, L=250} -> 순서 유지

TreeMap<String, Integer> treeMap = new TreeMap<>();
treeMap.put("Z", 5);
treeMap.put("A", 15);
treeMap.put("F", 25);
treeMap.put("L", 250);
System.out.println("treeMap = " + treeMap); // treeMap = {A=15, F=25, L=250, Z=5} -> 정렬해서 가짐

어떤 문장이 주어졌을 때 나오는 Character와 단어 수 구하기

String temp = "This is a great thing! This has never happened before.";
Map<Character, Integer> occurances = new HashMap<>();

char[] chars = temp.toCharArray();

for (char character : chars) {
    Integer integer = occurances.get(character);
    if (integer == null) {
        occurances.put(character, 1);
    }else{
        occurances.put(character, integer + 1);
    }
}
System.out.println("occurances = " + occurances);

// occurances = { =9, a=4, !=1, b=1, d=1, e=7, f=1, g=2, h=5, i=4, n=3, .=1, o=1, p=2, r=3, s=4, T=2, t=2, v=1}
String temp = "This is a great thing! This ahs never happened before.";
Map<String, Integer> stringOccurances = new HashMap<>();
String[] words = temp.split(" ");

char[] chars = temp.toCharArray();

for (String word : words) {
    Integer integer = stringOccurances.get(word);
    if (integer == null) {
        stringOccurances.put(word, 1);
    }else{
        stringOccurances.put(word, integer + 1);
    }
}
System.out.println("stringOccurances = " + stringOccurances);

// stringOccurances = {a=1, never=1, before.=1, This=2, is=1, great=1, ahs=1, thing!=1, happened=1}


정리

Hash → 이걸 보게 되면 순서도 없고 정렬도 되어있지 않다.

Linked → 요소들이 서로 연결되어 있다. 순서는 확실히 유지된다. 정렬된 방식으로 저장하지는 않는다.

Tree→ 데이터가 트리 구조에 정렬된 상태로 저장된다. 기본적으로 정렬되어 있기 때문에 여러 메서드 사용할 수 있다.

728x90
반응형

'백엔드 > Java' 카테고리의 다른 글

[Java] 추상클래스 vs 인터페이스  (0) 2023.03.23
oop의 특징 (캡슐화, 상속, 추상화, 다형성)  (0) 2023.03.17
Array, ArrayList  (0) 2023.03.17
Java 실행 원리와 JVM  (0) 2022.11.13
static keyword 정적 키워드  (0) 2022.11.06
728x90
반응형

JVM은 다른 프로그램을 실행시키는 것이 목적이다. 두 가지 기능이 있다고 말할 수 있는데,

  1. 자바 프로그램이 어느 기기나 운영체제 상에서도 실행될 수 있도록 한다
    • 자바와 os 사이에서 중개자 역할을 수행하여 OS에 상관없이 재사용을 가능하게 해준다.
  2. 프로그램 메모리를 관리하고 최적화 한다

JVM은 코드를 실행하고, 해당 코드에 대해 런타임 환경을 제공하는 프로그램에 대한 사양이다.

JVM은 보통 어떤 기기상에서 실행되고 있는 프로세스, 특히 자바 앱에 대한 리소스를 대표하고 통제하는 서버를 지칭한다.

자바 애플리케이션을 클래스 로더를 통해 읽어들이고, 자바 API와 함께 실행하는 역할을 한다.

  1. 프로그램이 실행되면 JVM은 OS로부터 프로그램에 필요한 메모리를 할당받는다.
  2. javac(자바 컴파일러)가 자바 소스코드를 읽고, 자바 바이트코드(.class)로 변환한다.
  3. 변경된 class 파일들을 클래스 로더를 통해 JVM 메모리 영역으로 로딩한다.
  4. 로딩된 class 파일들은 Execution engine을 통해 해석된다.
  5. 해석된 바이트 코드는 메모리 영역에 배치되어 수행된다. 이 때 JVM은 쓰레드 동기화나 가비지 컬렉션 같은 메모리 관리 작업을 수행한다.

[클래스 로더] : JVM은 런타임 시에 처음으로 클래스를 참조할 때 해당 클래스를 로드하고 메모리 영역에 배치시키는데 이 동적 로드를 담당하는 부분이 바로 클래스 로더이다.

Runtime Data Access

: JVM이 운영체제 위에서 실행되면서 할당받는 메모리 영역이다.

PC 레지스터, JVM 스택, 네이티브 메서드 스택, 힙, 메서드 영역

  • PC 레지스터 : 스레드가 어떤 명령어로 실행되어야 할지 기록하는 부분 (JVM 명령의 주소를 가진다)
  • 스택 : 지역변수, 매개변수, 메서드 정보, 임시 데이터 등을 저장
  • 네이티브 메서드 스택 : 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역
  • 힙 : 런타임에 동적으로 할당되는 데이터가 저장되는 영역. 객체나 배열 생성이 여기에 해당함. ( 힙에 할당된 데이터들이 가비지 컬렉터의 대상이 된다. JVM 성능에 영향을 미친다 )
  • 메서드 영역 : JVM이 시작될 때 생성되고, JVM이 읽은 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드 및 메서드 코드, 정적 변수, 메서드의 바이트 코드 등을 보관한다.

Garbage Collection

C나 C++은 개발자가 직접 메모리를 관리했지만 자바에서는 JVM이 프로그램 메모리를 관리한다. JVM은 Garbage Collection이라는 프로세스를 통해 메모리를 관리하는데 참조되지 않은 객체들을 탐색하여 삭제하고 삭제된 객체의 메모리를 반환하고 힙 메모리를 재사용하는 과정을 거친다.

 

 

https://gyoogle.dev/blog/computer-language/Java/Java 를 참조했습니다

728x90
반응형
728x90
반응형

URL Class

  • java.net.URL을 사용한다
  • Final Class이므로 상속이 되지 않는다
  • Immutable 하므로 fields는 생성된 이후에 바꿀 수 없다
  • fields에는 protocol, host, path 등이 있다.
  • 생성자가 존재한다. 생성자로 새로운 URL을 만든다
    • public URL(String url) throws MalformedURLException → 지원되지 않는 protocol이나 URL 문법에 맞지 않으면 MalformedURLException에 속한다
    • public URL(String protocol, String host, int port, String file) throws MalformedURLException
  • 생성자는 생성된 URL이 유효한지 안한지 체크하지 않는다. 그래서 URL이 존재하지 않거나 존재하더라도 접속되지 않을 수도 있다.
  • URL syntax가 만족해야 하는 것은 일단 ‘:’ 가 있어야 한다. 세미콜론을 통해 schem이나 protocol을 구분한다. 뒤에 오는 것은 어떤지는 신경쓰지 않는다.

URL로 데이터를 얻는 방법

  • data를 얻는 함수를 알아보겠다
  • public InputStream openStream() throws IOException
    • 제일 많이 사용되고 기본이 된다.
    • Client와 Server의 handshaking도 실행한다
    • 읽어들일 수 있는 data에서 InputStream을 반환한다.
    • InputStream으로 읽는 data는 raw content이다. 즉, ASCII 텍스트 파일은 ASCII로 읽히고, image file은 binary image data로 읽힌다.
    • HTTP header나 protocol 관련 정보는 포함하지 않는다.
    • 한계 : 너무 상위 레벨이라서 생기는 한계가 있다.
      • openStream()은 URL이 text를 가리키고 있다고 가정한다. 그러나 실제로 URL은 image나 sound, video같은 다른 타입도 가리킬 수 있다.
      • text이더라도 server에서의 encoding과 receiver에서의 encoding이 다를 수도 있다.
      • 다른 OS이면 해석이 다를 수도 있다.
      • HTTP header도 encoding 정보가 있을 수 있는데 여기서는 위에 말한대로 HTTP header를 읽을 수 없다.
      • 그래서 openConnection()이 필요하다.
    // 인수로 url 아무거나 넣어서 테스트하면 된다.
    public class SourceViewer {
    
        public static void main(String[] args){
            if(args.length > 0){
                InputStream in = null;
                try{
                    // Open the URL for reading
                    URL u = new URL(args[0]);
                    in = u.openStream();
                    // buffer the input to increase performance
                    in = new BufferedInputStream(in);
                    // chain the InputStream to a Reader
                    Reader r = new InputStreamReader(in);
                    int c;
                    while((c=r.read())!=-1){
                        System.out.print((char)c);
                    }
                }catch (MalformedURLException ex){
                    System.err.println(args[0] + " is not a parseable URL");
                }catch (IOException ex){
                    System.err.println(ex);
                }finally {
                    if(in!=null){
                        try{
                            in.close();
                        }catch (IOException e){}
                    }
                }
            }
        }
    }
    
  • public URLConnection openConnection() throws IOException
    • 반환 객체로부터 InputStream을 얻어야 할 수 있다
    • openStream보다 low level control이다.
    • openStream은 openConnection과 getContent()을 동시에 하는 것이다.
  • public Object getContent() throws IOException
    • 어떤 type으로 data retrieve 할것인지 결정할 수 있다.
      • URL은 ASCII나 HTML file을 의미할 수 있다.
      • URL은 GIF나 JPEG같은 image를 의미할 수 있다. → java.awt.ImageProducer가 return된다.
    • header of data에 있는 Content type field를 본다.
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.*;
    
    public class ContentGetter {
        public static void main(String[] args){
            try{
                URL u = new URL("<https://www.oreilly.com>");
                Object o = u.getContent();
                InputStream r = (InputStream) o;
                int c;
                while((c=r.read())!=-1) System.out.print((char) c);
                r.close();
                System.out.println("I got a "+o.getClass().getName());
            }catch (MalformedURLException ex){
                System.err.println(args[0] +" is not a parseable URL");
            }catch (IOException ex){
                System.err.println();
            }
        }
    }
    
  • public Object getContent(Class[] classes) throws IOException
    • Class에 제공한 순서대로 object를 가져오라는 뜻이다.
    • 만약 제공되지 않는 type이면 null을 return한다.

URL을 쪼개어 보자

  • URL의 요소들
  • Getter 함수들
    • public String getProtocol()
    • public String getHost()
    • public int getPort() → port가 명시되어 있지 않으면 -1을 리턴한다.
    • public int getDefaultPort()
    • public String getFile()
      • 첫번째 ‘/’에서 ‘#’까지를 string 형태로 return한다.
    • public String getPath()
      • query를 포함하지 않은 path만 리턴한다.
    • public String getRef()
      • fragment identifier part를 return한다. fragment identifier가 없으면 null을 리턴한다.
    • public String getQuery()
    • public String getUserInfo()
      • username+password를 리턴한다.
    • public String getAuthority()
      • userInfo + host + port를 리턴한다.

URL의 Equality와 Comparison

  • 언제 2개의 URL이 동일하다고 간주되냐면 같은 resource가 같은 host, port, path에 같은 fragment identifier와 query string이 같으면 동일하다고 한다.
  • equals() : 완전히 동일해야 한다
  • sameFile() : equals()와 같지만 fragment identifier는 고려하지 않는다.

URI Class

  • URI 문법
    • scheme : scheme-specific-part:fragment
  • URI Class 와 URL Class 의 비교
    • URI는 resource의 순수하고 identification이기 때문에 data를 얻을 함수는 없다.
    • RFC에 대해 조금 더 철저하게 다룬다
    • URI class는 protocol이 상관이 없다.
  • Methods
    • public String getScheme()
    • public String getSchemeSpecificPart()
    • public String getRawSchemeSpecificPart()
    • public String getFragment()
    • public String getRawFragment()
    raw하게 반환한다는 것은 I/O 같은 것을 I%20O 이런식으로 반환한다는 것이다.
    • public boolean isAbsolute() : URI가 scheme이 있으면 true
    • public boolean isOpaque() : URI가 hierarchical하면 false
    • public String toString() : encode되지 않은 string 형태 → I/O
    • public String toASCIIString() : encode된 URI 형태 → I%20O
    예를 들면 space는 %20이나 +로 대체된다.
    • URLEncoder.encode(String s, String encoding)
      • 이 함수는 non-ASCII character들을 모두 encode해버린다
      • 문제는 모든걸 encoding해버려서 문제가 생길 수 있다.
    • URLDecoder.decode(String s, String encoding)
      • 모든 plus sign을 space로 바꾸고 모든 percent escape를 해당하는 character로 바꾼다
    GET method로 통신하기
  • public class DMoz { public static void main(String[] args){ String target = ""; for(int i=0;i<args.length;i++){ target+="args[i]+"" ";="" }="" target="target.trim();" querystring="" query="new" querystring();="" query.add("q",target);="" try{="" url="" u="new" url("<<a="" href="https://search.yahoo.com/search?p=java&fr=yfp-t&fp=1&toggle=1&cop=mss&ei=UTF-8">https://search.yahoo.com/search?p=java&fr=yfp-t&fp=1&toggle=1&cop=mss&ei=UTF-8>"); try(InputStream in = new BufferedInputStream(u.openStream())){ InputStreamReader theHTML = new InputStreamReader(in); int c; while((c=theHTML.read())!=-1){ System.out.print((char)c); } } }catch (MalformedURLException ex){ System.err.println(ex); }catch (IOException ex){ System.err.println(ex); } } } </args.length;i++){>
  • encode form으로 반환한다는 뜻이다.

Password-Protected된 사이트에 접근하기

  • Authenticator Class
    • 추상 클래스이다.
    • methods
      • public static void setDefault(Authenticator a)
      • 만약 URL class가 username과 password를 필요로 한다면 MyAuthenticator를 물어본다.
      • getPasswordAuthentication() 함수를 override 해야한다.
        • password 인증이 필요하면 호출한다.
    • HTTP 인증 과정
      • client가 authentication info가 없는 request를 보낸다
      • server가 401을 보낸다
      • client가 authentication infor가 있는 request를 보낸다
      • server가 200을 보낸다

 

728x90
반응형
728x90
반응형

 1. 커널과 인터페이스

커널Kernel은 프로세스 관리, 메모리 관리, 저장장치 관리와 같은 운영체제의 핵심적인 기능을 모아놓은 것이다.

자동차에 비유하자면 커널은 자동차의 엔진에 해당한다. 따라서 운영체의 성능은 커널이 담당한다고 생각하면 된다.

 

즉, OS의 핵심부분이고 항상 메모리에 적재되어 있다. 

 

스마트폰 운영체제에도 커널이 있다. 유닉스 운영체제의 커널을 이용하여서 만든 구글의 안드로이드가 그 예이다. 유닉스 운영체제는 다양한 제조사가 이용하도록 커널 소스코드가 공개되어있다.

 

같은 커널을 이용하더라도 다른 인터페이스가 장착이 가능하다. 다른 인터페이스가 장착된다면 다른 운영체제로 보이게 된다.

우리가 사용하는 운영체제도 커널은 같은데 인터페이스가 달라서 다른 운영체제처럼 보이는 것이 많다.

 

예를 들면 유닉스의 사용자 인터페이스는 셸shell을 사용하는데 여기에는 C셸, T셸, 배시셸 등 여러 종류가 있다. 스티브 잡스의 매킨토시의 MacOS도 유닉스 커널을 사용한다. 사람들은 좋은 인터페이스의 컴퓨터를 사용하려는 경향이 있다.

(이를 이용한 사람이 바로 스티브 잡스이다.)

 

왜 인터페이스를 사용하는 것일까??

프로세스는 개발된 프로그래밍 언어가 모두 다르고, 시스템 콜(아래에서 설명하겠다)을 호출하는 방식이 모두 다르기 때문에 중간에 추상 계층이 필요하다. 

 

운영체제와 커널의 차이는 무엇일까?

아래 그림에서도 볼 수 있듯이 커널은 운영체제에 포함되는 하나의 모듈의 개념이다.

운영체제는 커널을 포함해, 컴퓨터 시스템을 총괄하는 개념이다. 



 

2. 시스템 호출과 디바이스 드라이버(커널 내부에 있는 것들이다)

1) 시스템 호출 System Call

시스템 콜은 커널이 자신을 보호하기 위해 만든 인터페이스이다. 커널은 사용자나 응용프로그램이 자원에 직접 접근하는 것을 차단한다.

자원을 이용하려면 시스템 호출이라는 인터페이스를 이용하여서 접근해야 한다. 프로세스가 OS 커널이 제공하는 서비스를 이용하고 싶을 때, 시스템 콜을 이용해서 실행한다.

 

직접 접근해야 더 편리할 거 같은데 왜 이렇게 할까?

직접 접근을 하면 사용자가 모든 것을 처리해야 한다. 그리고 보안도 취약해진다. 그래서 더 편리하고 안전한 시스템 호출이라는 방식을 사용한다. 예를 들어, C언어에서 printf 함수도 시스템 호출 함수의 한 종류이다. 

 

시스템 콜이 호출되면 커널은 CPU에게 Interrupt를 발생하고, CPU는 다음 명령을 실행할 때 해당 내용을 체크하고, 해당 커널 코드를 실행하게 된다. 

 

API와 SDK

API란 Application Programming Interface의 약자로 응용프로그램 인터페이스라고 한다. 응용프로그램이 자신과 관련된 프로그램을 만들 수 있도록 제공하는 인터페이스이다. 예를 들어 포토샵을 이용할 때, 포토샵 자체가 수백가지 필터를 모두 제공하지 않는다. 자신이 필요하면 포토샵은 필터를 개발하려는 사람들을 위해 다양한 프로그래밍 인터페이스를 제공한다.

UI가 사용자와 사용자가 다룰 대상을 연결한다면, API는 프로그램과 또 다른 프로그램을 연결해주는 일종의 다리라고 볼 수 있다. 

 

SDK System Developer's Kit는 프로그램 개발자를 위해 API 및 API 사용 메뉴얼 뿐만 아니라 프로그램 개발에 필요한 코드편집기와 에뮬레이터(다른 프로그램이나 장치를 모방하는 컴퓨터 프로그램) 같은 개발용 응용 프로그램까지 하나로 묶어서 배포하는 개발 툴이다.

SDK를 활용하여 개발자는 이러한 시스템, 프로그래밍 언어에 따라 애플리케이션을 개발할 수 있다.

예를 들어 안드로이드 SDK인 Android Studio가 있다.

 

 2) 드라이버 

커널과 하드웨어의 인터페이스는 드라이버driver가 담당한다. 커널이 모든 하드웨어에 맞는 인터페이스를 다 개발하기는 어렵다. 커널은 입출력의 기본적인 부분만 제작하고 하드웨어의 특성을 반영한 소프트웨어를 하드웨어 제작자에게 받아 커널이 실행될 때 함께 실행되도록 한다.

이를 디바이스 드라이버 라고 한다.

예를 들어 그래픽카드나 프린터 같은 장치를 설치하고 해당 장치의 디바이스 드라이버를 설치해 주어야 한다.

 

3. 커널의 구성

커널의 기능은 프로세스 관리, 메모리 관리, 파일 시스템 관리, 입출력 관리, 프로세스간 통신 관리이다.

커널은 이러한 기능을 `어떻게` 구현하는가에 따라 단일형 구조 커널, 계층형 구조 커널, 마이크로 구조 커널로 구분된다.

 

1) 단일형 구조 커널 monolithic architecture

초창기 커널 구조로 커널의 핵심 기능을 구현하는 모듈들이 구분없이 하나로 구성된다. 

ex) MS-DOS, VMS, 초기의 유닉스

 

프로그램 만들 때 main()에 모두 때려박은거라고 생각하면 된다. 프로그래밍에서처럼 모듈간의 통신 비용이 줄어들어 효율적으로 운영가능하다(속도가 빠름).

그런데 모든 모듈이 묶여잇어 버그나 오류 처리가 어렵다. 작은 결함이 크게 확산이 가능하다.

 

2) 계층형 구조 커널 layered architecture

비슷한 구조기능을 가진 모듈을 묶어서 하나의 계층으로 만든다. 디버깅 하기 쉽다

ex) 마이크로소프트의 윈도우 운영체제

 

3) 마이크로 구조 커널

커널이 계속 커져서 이제 오류가 잡기가 어려워졌다. 그래서 생긴 것이 마이크로 구조 커널이다.

프로세스 관리, 메모리 관리, 프로세스 간 통신 관리 등 가장 기본적인 기능을 제공하고 많은 부분이 사용자 영역에 구현되어 있다.

 

각 모듈은 세분화되어 존재하고 모듈 간의 정보교환은 프로세스 간 통신을 이용하여 이루어진다. 

ex) 마하Mach -> 애플의 PC운영체제인 OS X와 모바일 iOS의 커널로 사용된다.

 

4. 가상 머신

맨 첨에 C언어로 유닉스 운영체제를 만들었는데, C언어는 호환성이 떨어진다.

윈도우 운영체제와 호환이 안되었던 것이다. 이걸 해결한 언어가 자바Java언어이다.

 

작동원리는 가상 머신virtual machine 을 만들고 그 위에서 작동하도록한다. 가상 머신은 운영체제와 응용 프로그램 사이에서 작동하는 프로그램이다. 

ex) JVM (Java Virtual Machine)

728x90
반응형

'CS > 운영체제' 카테고리의 다른 글

병렬처리  (0) 2020.10.26
인터럽트  (0) 2020.10.26
[운영체제] 컴퓨터 기본 구성  (0) 2020.10.13
[운영체제] 운영체제의 역사  (0) 2020.10.09
[운영체제] 운영체제란 무엇일까...  (0) 2020.10.07

+ Recent posts