What are Protocol Buffers?
Protocol buffer는 structured data(구조화 데이터)를 seriailize(직렬화)하기위한 Google의 언어 중립적, 플랫폼 중립적이며 확장 가능한 메커니즘입니다.
XML보다 작지만, XML보다 빠르고 단순합니다.
IDL(Interface Definition Language)로서 data structure를 정의한 다음, .proto 파일을 protocol buffer compiler(protoc)를 이용해 compile 합니다. Complie된 소스 코드를 사용하여 다양한 데이터 스트림에서 다양한 언어로 다양한 구조의 데이터를 쉽게 읽고 쓸 수 있습니다.
프로토콜 버퍼는 현재 Java, Python, Objective-C 및 C ++에서 생성 된 코드를 지원합니다. 새로운 proto3 버전을 사용하면 proto2에 비해 더 많은 언어(Dart, Go, Ruby 및 C #)을 사용할 수 있습니다.
Proto3 Guide
Protocol buffers는 크게 proto2와 proto3가 있지만 grpc에선 최신 버전인 proto3를 사용하기에 proto3를 보겠습니다.
Defining 메세지 type
먼저 아주 간단한 예를 봅시다.
각 요청에 쿼리 문자열, 관심있는 특정 결과 페이지 및 페이지 당 여러 결과가있는 검색 요청 메시지 형식을 정의한다고 가정 해 보겠습니다. 메시지 유형을 정의하는 데 사용하는 .proto 파일은 다음과 같습니다.
//syntax가 없으면 자동으로 proto2로 인식됨
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
SearchRequest 메시지 정의는이 유형의 메시지에 포함하려는 각 데이터에 대해 하나씩 세 개의 필드 (이름 / 값 쌍)를 지정합니다. 각 필드에는 이름과 유형이 있습니다.
Specifying Field Types
위의 예시에서 나온 Field types은 모두 scalar types입니다.(int32, string)
하지만 scalar type 뿐만 아니라 enum과 같은 다른 field type도 선언 가능합니다.
Assigning Field Numbers
각 field는 unique number를 갖습니다.
이 field number는 메세지 binary format에서 field를 식별하는데 사용됩니다. 즉 field number가 key가 되고 field type이 value가 되는 해시 구조를 지닙니다.
Field numbers의 값이 1~15 사이면 1 byte로 encoding 됩니다.
Field numbers의 값이 16~2047이면 2 bytes로 encoding 됩니다.
그렇기에 Field numbers의 값을 1~15 사이로 encoding 하는것이 효율적입니다.
Specifying Field Rules
메세지 fields는 다음 중 한 가지를 따릅니다.
- singular: well-formed된 메세지는 0 or 1개를 가질 수 있습니다.
- repeated: 이 field는 0을 포함해 여러번 반복될 수 있습니다.
Adding 메세지 Types
하나의 .proto 파일안에 다중 메세지 type이 정의될 수 있습니다. 이러한 방식은 다양한 관계를 표현하는데 도움 됩니다.
// comment(주석)은 C, C++ 스타일
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...}
What’s Generated From Your .proto?
.proto 파일을 protocol buffer compiler에 의해 complied되면, 메세지는 serializing되어 output stream으로 나가고 parsing되어 input stream으로 들어옵니다. 대표적인 3가지는 다음과 같습니다.
- C ++의 경우 컴파일러는 파일에 설명 된 각 메세지 type에 대한 class와 함께 각 .proto에서 .h 및 .cc 파일을 생성합니다.
- Java의 경우 compiler는 각 메세지 type에 대한 class와 메세지 class instance 작성을위한 builder class가있는 .java 파일을 생성합니다.
- Python은 조금 다릅니다. Python compiler는 .proto에 각 메세지 type의 static discriptor가 있는 모듈을 생성 한 다음, meta class와 함께 런타임에 필요한 Python data access class를 작성하는 데 사용됩니다.
Encoding
Protocol buffer compiler가 encoding하는 방식을 알아봅시다.
Base 128 Varints
Varints는 1 or 그 이상의 bytes를 사용해서 integer를 serializing하는 방법입니다. 작은 숫자는 작은 bytes로 encoding 됩니다. 1 bytes 중에서 7 bits만 숫자 표현에 사용되고 맨 앞의 숫자는 msb(most significant bit)로 사용됩니다.
예시로 보자면, 숫자 1을 encoding 하면 다음과 같으며, msb는 set되지 않습니다.
0000 0001
숫자 300은 다음과 같습니다
1010 1100 0000 0010
-> 010 1100 000 0010 // msb 버림
-> 000 0010 010 1100 // 7bits의 두 그룹을 reverse함
-> 000 0010 ++ 010 1100 // 두 그룹 합침
-> 100101100
-> 256 + 32+ 8 + 4 = 300
메세지 Structure
메세지는 key-value 쌍입니다. 메세지의 binary version은 field number를 key로 사용합니다. 각 field의 이름과 선언 된 type은 메세지 type의 정의를 참조하여 decoding 끝에서만 확인할 수 있습니다.
사용 가능한 유형은 다음과 같습니다.
TypeMeaningUsed For
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded 메세지s, packed repeated fields |
3 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
자료형, 자료구조
Scalar Value Types
scalar 메세지 field는 다음 중 하나의 type을 가집니다.
.proto TypeNotesC++ TypeJava TypePython Type
double | double | double | float | |
float | float | float | float | |
int32 | Uses variable-length encoding.Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int |
int64 | Uses variable-length encoding.Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long |
uint32 | Uses variable-length encoding. | uint32 | int | int/long |
uint64 | Uses variable-length encoding. | uint64 | long | int/long |
sint32 | Uses variable-length encoding.Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int |
sint64 | Uses variable-length encoding.Signed int value.These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long |
fixed32 | Always four bytes.More efficient than uint32 if values are often greater than 228. | uint32 | int | int/long |
fixed64 | Always eight bytes.More efficient than uint64 if values are often greater than 256. | uint64 | long | int/long |
sfixed32 | Always four bytes. | int32 | int | int |
sfixed64 | Always eight bytes. | int64 | long | int/long |
bool | bool | boolean | bool | |
string | A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232. | string | String | str/unicode |
bytes | May contain any arbitrary sequence of bytes no longer than 232. | string | ByteString | str |
Enumerations
메세지 type을 정의 할 때, enum type을 쓸 수 있습니다.
예를 들어, 각 SearchRequest에 대해 Corpus field를 추가하려고하는데, Corpus는 UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS, VIDEO field를 가집니다. 다음 예제에서는 가능한 모든 값과 Corpus라는 enum을 추가했습니다
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
예시에서와 같이 모든 eunum type의 시작 defulat value는 0입니다.
만약 default value를 겹쳐서 사용하고 싶다면 option에 allow_alias를 사용하면 됩니다.
message Mymessage1 {
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
}
message Mymessage2 {
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning 메세지 outside.
}
}
enum 타입의 field들은 int32를 사용해야 합니다. 왜냐하면 enum value들은 varint encoding을 따르기 때문입니다.
Nested Types
메세지 type을 정의할 때, nested(중첩된)하게 선언도 됩니다.
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
Packages
optional하게 package specifier를 추가할 수 있습니다. 이를 통해 메세지 type 간에 충돌을 피할 수 있습니다.
package foo.bar;
message Open
{ ... }
Defining Services
만약 gRPC와 함께 사용하고 싶다면, service 구문을 사용하면 됩니다. protocol buffer compiler가 service interface code와 stubs를 언어에 맞게 자동으로 생성해 줍니다.
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
JSON Mapping
Proto3는 JSON으로의 canonical encoding도 지원합니다. 이를 통해 보다 쉬운 시스템 간의 통신 구현이 가능합니다.
Proto와 JSON
Person이라는 객체를 만들고, 그 객체를 JSON 포맷으로 만든다고 해봅시다.(UTF-8 인코딩 가정시)
{
"userName":"Martin",
"favouriteNumber":1337,
"interests":["daydreaming","hacking"]
}
데이터 크기를 보면 공백을 빼도 총 80byte를 사용했습니다. 그렇다면 Protocol Buffer를 사용했을 때는 다음 그림과 같습니다.
message Person {
required string user_name = 1;
optional int64 favourite_number = 2;
repeated string interests = 3;
}
그림의 설명처럼 핵심은 userName과 같은 string을 field number로 대체한 것이 핵심 입니다. 따라서 proto를 이용해 데이터를 표현하게되면 같은 데이터여도 33byte만으로 표현할 수 있게 됩니다.
Proto의 장점
1. 통신이 빠름.
같은 데이터를 보낼시, 데이터의 크기가 작으니까 같은 시간에 더 많은 데이터를 보낼 수 있습니다.
2. 파싱을 할 필요가 없음
JSON 포맷으로 온 데이터를 다시 객체로 파싱해서 사용해야하지만, Protocol Buffer는 Byte Stream을 Proto file이 읽어 파싱할 필요 없습니다.
Proto의 단점
1. 인간이 읽기 불편함.
JSON 포맷의 경우, 사람이 읽기 편합니다. 반대로 protocol buffer가 쓴 데이터는 proto 파일이 없으면 아예 무슨 의미인지 모릅니다.이 문제 때문에 외부 API로 쓰이기에는 문제가 있습니다. (모든 클라이언트가 proto파일을 가지고 있어야 하기에)
그래서 내부 서비스간의 데이터 교환에서 주로 쓰입니다. (API Gateway를 이용해 REST API로 바꿔주는 방식도 존재)
2. proto 문법을 배워야 함
proto 파일 작성을 위한 문법을 배워야하며, proto파일을 한 번 정의했다 변경되면 proto파일을 쓰는 애플리케이션은 proto 파일이 다시 업데이트 되야한다는 단점이 있습니다.
'Programming Language > Go' 카테고리의 다른 글
grpc 소개 (0) | 2022.11.15 |
---|---|
[go 모듈 소개 - 2. logrus] 더 자세한 로깅 모듈 (0) | 2022.11.15 |
[go 모듈 소개 - 1. gorm] Golang을 위한 ORM 라이브러리 (0) | 2022.11.15 |
목차
1. logrus란?
2. logrus의 특징
3. logrus 사용 예시
Logrus란?
logrus는 golang에서 사용할 수 있는 로깅 모듈 중의 하나로, docker와 Prometheus와 같은 많은 유명 오픈 소스 프로젝트가 이를 사용하여 로그를 기록하는 것으로 알려졌다.
Logrus의 특징
logrus는 다음과 같은 특징이 있다.
- golang 표준 라이브러리의 로깅 모듈과 완벽하게 호환된다. logrus에는 6가지 로그 레벨이 존재하며, 이는 golang 표준 라이브러리의 로그 모듈 API의 상위 집합이다. 프로젝트에서 표준 라이브러리의 log 모듈을 사용하는 경우 저렴한 비용으로 logrus로 마이그레이션할 수 있다.
- 확장 가능한 hook 매커니즘: 사용자가 hook을 통해 로컬 파일 시스템, 표준 출력, logstash, elastic search 또는 MQ와 같은 위치에 로그를 배포하거나 hook을 통해 로그 내용 및 형식을 정의할 수 있다.
- 선택적 로그 출력 형식: logrus에는 jsonformatter 및 textformatter의 두 가지 기본 제공 로그 형식이 있다. 이 두 형식으로도 충분하지 않은 경우 인터페이스 포맷터를 직접 구현하여 고유한 로그 형식을 정의할 수도 있다.
- 필드 매커니즘: logrus는 긴 메세지를 통한 로깅보다 필드 매커니즘을 통해 상세하고 구조화된 로깅을 지원하고 권장한다.
Logrus 사용 예시
먼저, logrus를 사용하기 위해 모듈을 가져온다.
$ go get -u github.com/sirupsen/logrus
#1 Logrus를 사용하는 간단한 예
package main
import log "github.com/sirupsen/logrus"
func main() {
log.WithFields(log.Fields{
"animal": "tiger",
"habitat": "mountain",
}).Info("A tiger appears")
}
결과
logrus를 사용하는 가장 간단한 예시이다. 이처럼 필드를 통해 로그의 내용을 표현하고, 로그를 7단계로 구분한다.
기존 6단계에서 trace가 추가되어 7단계로 변경되었다.
logrus 로그의 7단계
log.Trace("Something very low level.") // 1단계: Trace
log.Debug("Useful debugging information.") // 2단계: Debug
log.Info("Something noteworthy happened!") // 3단계: Info
log.Warn("You should probably take a look at this.") // 4단계: Warn
log.Error("Something failed but I'm not quitting.") // 5단계: Error
log.Fatal("Bye.") // 6단계: Fatal, 로깅 이후 os.Exit(1) 호출
log.Panic("I'm bailing.") // 7단계: Panic, 로깅 이후 Panic() 호출
위의 코드 블록에서 다소 오해가 발생할 여지가 있는 부분이 있다. 구분을 위해, Trace를 1 단계라고 명시했지만, 낮다를 표현하고자 하였을 뿐 정해진 바는 없다. 즉, Panic이 1 단계일 수도 있다. Trace에서 Panic으로 7단계 구분이 이뤄졌다는 점만 기억하면 되겠다.
#2 다양한 옵션 설정도 가능하다.
대표적으로 로그 형식과 로그 출력 방식을 지정할 수 있다. 또 로그 레벨의 하한을 결정하여, 그 이하의 로그는 출력하지 않도록 할 수도 있다. 그 외에도 여기에는 등장하지 않았지만, 로그와 관련된 훅 같은 옵션도 적용할 수 있으나, 주제가 무거우니 향후 가능하다면 별도의 포스팅으로 분리하는 것으로 하자.
package main
import (
log "github.com/sirupsen/logrus"
"os"
)
func init() {
// 로그 형식을 JSON 형식으로 지정
log.SetFormatter(&log.JSONFormatter{})
// log 출력을 표준 출력으로 설정 (기본 설정은 stderr, 표준 에러)
log.SetOutput(os.Stdout)
// 로그 레벨을 warn 단계 위로 설정
log.SetLevel(log.WarnLevel)
}
func logrusOne() {
log.WithFields(log.Fields{
"animal": "tiger",
"distance": 100,
}).Info("A tiger appears")
log.WithFields(log.Fields{
"animal": "tiger",
"distance": 10,
}).Warn("A tiger coming to you")
log.WithFields(log.Fields{
"animal": "tiger",
"distance": 0,
}).Fatal("A tiger wants to play with you. Good Luck.")
}
func main() {
logrusOne()
}
결과
# 3 logrus는 log를 기록할 인스턴스를 정의하여, 각 인스턴스에 대해서 다른 옵션을 적용하는 것도 가능하다.
앞서 logrus에서 log에 대한 옵션 설정이 어떻게 이뤄지는지를 보였다. 여기에 더해 logrus의 logrus.Entry 인스턴스를 지정하여, 각각의 인스턴스마다 다른 옵션을 적용할 수도 있다.
package main
import (
log "github.com/sirupsen/logrus"
"os"
)
// Logrus 모듈에서는 new() 함수를 통해 logrus.Entry 인스턴스를 만들 수 있다.
// 프로젝트 내에서 원하는 수만큼 logrus.Entry인스턴스를 만들 수 있다.
var (
logInstanceOne = log.New()
logInstanceTwo = log.New()
)
func init() {
// 생성한 logrus 인스턴스의 출력이 어떻게 이뤄질지를 결정한다.
// logrus 인스턴스 출력은 io.writer라면 모두 가능하다.
logInstanceOne.Out = os.Stdout
logInstanceTwo.Out = os.Stderr
// 생성한 logrus 인스턴스의 출력 형식이 어떻게 이뤄질지를 결정한다.
// 이와 비슷하게, logrus 인스턴스 별로 "로그 레벨" 또는 "훅"을 설정할 수 있다.
logInstanceOne.Formatter = &log.JSONFormatter{}
logInstanceTwo.Formatter = &log.TextFormatter{}
}
func main() {
logInstanceOne.WithFields(log.Fields{
"animal": "tiger",
"habitat": "mountain",
}).Info("A tiger appears")
logInstanceTwo.WithFields(log.Fields{
"animal": "tiger",
"habitat": "mountain",
}).Info("A tiger appears")
}
결과
#4 logrus의 필드
logrus에서는 로깅을 위한 장황한 메세지를 사용하는 것을 권장하지 않는다. 대신 Fields 형식을 통한 상세하고 구조화된 로깅을 권장한다.
기존 표준 log 라이브러리를 사용했을 경우의 로깅
log.Fatalf("Failed to send event %s to topic %s with key %d", event, topic, key)
logrus 모듈에서 권장하는 필드를 활용한 로깅
log.WithFields(log.Fields{
"event": event,
"topic": topic,
"key": key,
}).Fatal("Failed to send event")
WithFields 메서드를 사용하면 좀 더 상세한 정보와 함께 원하는 방식으로 로깅할 수 있겠으나, WithFields 메서드는 어디까지나 필수는 아니다. 몇몇 시나리오에서는 필드가 필요하지 않을 수 있고, 기존 log 모듈에서도 가능하듯 간단한 메시징만 필요할 수도 있다.
애플리케이션 내부에서는 종종 고정된 필드와 함께 로깅을 해야하는 상황이 있을 수 있다. 예를 들어 HTTP 요청을 처리할 경우, 모든 로그는 request_id와 user_ip가 존재한다.
이 경우, 매 번 log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})를 붙이는 것을 피하기 위해, logrus.Entry 인스턴스를 생성하고, 해당 인스턴스에 Field를 디폴트로 설정할 수 있다.
requestLogger := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
requestLogger.Info("something happened on that request")
requestLogger.Warn("something not great happened")
'Programming Language > Go' 카테고리의 다른 글
grpc 소개 (0) | 2022.11.15 |
---|---|
protocol 버퍼에 대한 소개 (0) | 2022.11.15 |
[go 모듈 소개 - 1. gorm] Golang을 위한 ORM 라이브러리 (0) | 2022.11.15 |
ORM(Object Relation Mapping)이란?
- 객체와 RMDB의 데이터를 자동으로 매핑해주는 것을 말한다.
- 객체 지향 프로그래밍은 클래스를 사용하고, 관계형 데이터베이스는 테이블을 사용한다. (golang은 struct로 클래스를 대신한다)
- 객체 모델과 관계형 모델 간의 불일치가 존재한다.
- ORM을 통해 객체 간의 관계를 바탕으로 SQL을 자동으로 생성하여 불일치를 해결한다.
- 객체 지향 프로그래밍은 클래스를 사용하고, 관계형 데이터베이스는 테이블을 사용한다. (golang은 struct로 클래스를 대신한다)
- 매핑된 정보를 바탕으로 자동으로 SQL을 작성해준다.
- Object를 통해 간접적으로 데이터베이스를 다룰 수 있다.
- Object를 통해 간접적으로 데이터베이스를 다룰 수 있다.
ORM의 장단점
장점
- 객체 지향적인 코드이기에 비즈니스 로직에 더 집중할 수 있다.
- ORM을 사용하면 SQL Query가 아닌 작성 중인 언어로 DB에 접근하여 데이터를 접근할 수 있다.
- 각종 객체에 대한 코드를 별도로 작성하기 때문에 코드의 가독성이 올라간다.
- SQL의 절차적이고 순차적인 접근이 아닌 객체 지향적인 접근으로 인해 생산성이 증가한다.
- ORM을 사용하면 SQL Query가 아닌 작성 중인 언어로 DB에 접근하여 데이터를 접근할 수 있다.
- 재사용 및 유지보수의 편리성이 증가한다.
- ORM은 독립적으로 작성되어 있고, 해당 객체들을 재활용할 수 있다.
- MVC 모델을 사용할 경우 Model에서 가공된 데이터를 Controller에 의해 뷰와 합쳐지는 형태로 디자인 패턴을 견고하게 다지는데 유리하다.
- 매핑 정보가 명확하여, ERD를 보는 것에 대한 의존도를 낮출 수 있다.
- ORM은 독립적으로 작성되어 있고, 해당 객체들을 재활용할 수 있다.
- DBMS에 대한 종속성이 줄어든다.
- 객체 간의 관계를 바탕으로 SQL을 자동으로 생성하기 때문에 RDBMS의 데이터 구조와 코드의 객체지향 모델 사이의 간격을 좁힐 수 있다.
- 대부분의 ORM 솔루션은 DB에 종속적이지 않다.
- Object에 집중함으로써 극단적으로는 DBMS를 교체하는 거대한 작업에도 비교적 적은 리스크와 시간이 소요된다.
- 객체 간의 관계를 바탕으로 SQL을 자동으로 생성하기 때문에 RDBMS의 데이터 구조와 코드의 객체지향 모델 사이의 간격을 좁힐 수 있다.
단점
- 완벽한 ORM으로만 서비스를 구현하기가 어렵다.
- 사용하기는 편하지만 설계는 매우 신중하게 해야한다.
- 프로젝트의 복잡성이 커질 경우 난이도 또한 올라갈 수 있다.
- 잘못 구현된 경우 속도 저하 및 심각할 경우 일관성이 무너지는 문제점이 생길 수 있다.
- 일부 자주 사용되는 대형 쿼리는 속도를 위해 SP를 쓰는 등의 별도의 튜닝이 필요한 경우가 있다.
- DBMS의 고유 기능을 사용하기 다소 어렵다. (-> 특정 DBMS의 고유 기능을 이용한다면, 이식성이 저하된다는 문제점이 있다.)
- 사용하기는 편하지만 설계는 매우 신중하게 해야한다.
- 프로시저가 많은 시스템이라면 ORM의 객체 지향적인 장점을 활용하기 어렵다.
gORM이란?
gORM은 golang에서 사용 가능한 ORM 라이브러리 이다.
DB 환경 세팅
1) root 계정으로 mysql 접속
$sudo su - # su를 통해 관리자 권한 가져옴
$mysql -u root # mysql에 root 사용자로 접속 (비밀번호를 설정해뒀다면, mysql -u roor -p)
2) 사용자 생성 및 권한 후여
# 유저 생성, CREATE USER '{username}'@'{hostname}' IDENTIFIED BY '{password}'
CREATE user 'tester'@'localhost' IDENTIFIED BY 'password123'
# 모든 권한을 해당 유저, 호스트 조합에 허용
GRANT ALL PRIVILLEGES ON *.* TO 'tester'@'localhost';
# grant 테이블을 reload하여 변경 사항을 즉시 반영하도록 함
FLUSH PRIVILLEGES
3) 생성 확인
SELECT user, host FROM user;
결과
+------------------+-----------+
| USER | HOST |
+------------------+-----------+
| mysql.infoschema | localhost |
| mysql.session | localhost |
| mysql.sys | localhost |
| root | localhost |
| tester | localhost |
+------------------+-----------+
관련 패키지 설치
1) gORM 설치
go의 프로젝트 경로에서 다음 명령어를 작성한다.
$ go get -u gorm.io/gorm
해당 명령어를 수행하면, 현재 환경 기준으로 GOPATH/pkg/mod/gorm 에 해당 패키지가 설치되고 프로젝트 경로에서는 go.mod와 go sum에서 이를 관리하게 된다.
-u 명령어의 경우, 패키지 및 관련된 종속성을 업데이트하는 명령어로 go get과 세트로 자주 쓰이는 옵션이다.
2) gORM 용 MySQL 드라이버 설치
데이터 베이스 별로 해당하는 드라이버가 필요하다.
$ go get -u github.com/go-sql-driver/mysql
3. 쿼리 로그 설정
- 작성된 쿼리를 추적하기 위해, sql로 수행되는 쿼리를 파일에 로그로 담도록 지정
- gorm을 사용할 경우, 실제 sql이 어떻게 수행되고 있는지가 궁금할 수 있다.
mysql> SET GLOBAL general_log = 1;
Query OK, 0 rows affected (0.03 sec)
mysql> SELECT @@log_output, @@general_log, @@general_log_file;
+--------------+---------------+-----------------------------------------------------+
| @@log_output | @@general_log | @@general_log_file |
+--------------+---------------+-----------------------------------------------------+
| FILE | 1 | /usr/local/var/mysql/youngmki-MacBookPro.log |
+--------------+---------------+-----------------------------------------------------+
로그를 사용하도록 설정하고, 설정 여부를 확인했다.
다음 명령어를 통해 실제 로그 파일이 존재하는지를 확인할 수 있다.
$cd /usr/local/var/mysql && find "youngmki-MacBookPro.log"
DB 연결 예제
테스트를 위해 다음과 같이 main.go를 작성했다.
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
// "{username}:{password}@tcp(127.0.0.1)
dsn := "tester:password123@tcp(127.0.1:3306)/"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("could not connect to the database")
}
_ = db // 일단 컴파일이 되도록 설정
}
간단한 웹서버 예제
먼저 fiber라는 웹서버 라이브러리를 사용할 예정이므로, 다음 명령어를 통해 모듈을 설치한다.
$ go get -u "github.com/gofiber/fiber/v2"
그 다음으로는 mysql 내에 테이블을 생성하고자 한다.
gorm에서는 AutoMigrate라는 메서드를 통해, struct를 테이블로 옮길 수 있도록 지원한다.
그러기 위해서 먼저 다음을 models/user.go 에 작성했다.
package models
type User struct {
Id uint
Name string
Email string
Password []byte
}
그 다음에 이를 database/connection.go 라는 파일에서 DB 연결과 함께 다뤄줬다.
package database
import (
"github.com/fatih/color"
"github.com/mukmookk/simple-rest/models"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
func Connect() {
// "{username}:{password}@tcp(127.0.0.1)
dsn := "tester:password123@tcp(127.0.1:3306)/"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("could not connect to the database")
}
DB = connection
color.Green("Connection Opened to Database... Success!")
connection.AutoMigrate(&models.User{})
color.Green("Database Migrated... Success!")
}
여기서 color라는 모듈을 추가로 사용했는데, 터미널 출력에 다음처럼 색을 깔끔하게 입혀준다.
routes/routes.go 라는 파일을 생성하고, REST-API 메소드, url, handler를 정의해주었다.
package routes
import (
"github.com/gofiber/fiber/v2"
"github.com/mukmookk/simple-rest/models"
)
func Setup(app *fiber.App) {
app.Post("/api/register", controllers.Register)
}
여기까지 작성했다면, 이젠 실제 핸들러를 구현해야 한다.
controller/controller.go를 생성하고 핸들러를 구현한다.
func Register(c *fiber.Ctx) error {
var data map[string]string
err := c.BodyParser(&data)
if err != nil {
return err
}
password, _ := bcrypt.GenerateFromPassword([]byte(data["password"]), 14)
user := models.User{
Name: data["name"],
Email: data["email"],
Password: password,
}
database.DB.Create(&user)
return c.JSON(user)
}
먼저 map을 정의하여 들어온 JSON을 담아준다.
-> c.BodyParser(&data)
비밀번호의 경우, 그냥 넣어지지가 않는다. 때문에 한번 해쉬 변환을 거쳐준다.
-> bcrypt.GenerateFromPassword([]byte(data["password"]), 14)
data에 담아진 정보를 model/user.go에 정의한 struct 형식으로 옮겨주고,
실제 ROW를 생성한다.
-> database.DB.Create(&user)
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/mukmookk/simple-rest/database"
"github.com/mukmookk/simple-rest/routes"
)
func main() {
app := fiber.New()
database.Connect()
routes.Setup(app)
app.Listen(":3000")
}
서버를 실행시키고
$ go run main.go
POSTMAN으로 동작 여부를 다음과 같이 확인할 수 있다.
id의 경우는 autoincrement이기에 여러번 실행하였더니 5가 되었다.
앞서 설정한 로그 파일을 출력해보면 다음과 같은 쿼리가 실행되었음을 알 수 있다.
$ cat /usr/local/var/mysql/gim-yeongmug-ui-MacBookPro.log
/usr/local/Cellar/mysql/8.0.27_1/bin/mysqld, Version: 8.0.27 (Homebrew). started with:
Tcp port: 3306 Unix socket: /tmp/mysql.sock
Time Id Command Argument
2022-01-27T15:25:46.193725Z 62 Query SELECT VERSION()
2022-01-27T15:25:46.200413Z 62 Query SELECT DATABASE()
2022-01-27T15:25:46.236453Z 62 Prepare SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE ? ORDER BY SCHEMA_NAME=? DESC limit 1
2022-01-27T15:25:46.238041Z 62 Execute SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE 'testdb%' ORDER BY SCHEMA_NAME='testdb' DESC limit 1
2022-01-27T15:25:46.246686Z 62 Close stmt
2022-01-27T15:25:46.262921Z 62 Prepare SELECT count(*) FROM information_schema.tables WHERE table_schema = ? AND table_name = ? AND table_type = ?
2022-01-27T15:25:46.262984Z 62 Execute SELECT count(*) FROM information_schema.tables WHERE table_schema = 'testdb' AND table_name = 'users' AND table_type = 'BASE TABLE'
2022-01-27T15:25:46.276699Z 62 Close stmt
2022-01-27T15:25:46.276761Z 62 Query SELECT DATABASE()
2022-01-27T15:25:46.278598Z 62 Prepare SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE ? ORDER BY SCHEMA_NAME=? DESC limit 1
2022-01-27T15:25:46.278646Z 62 Execute SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE 'testdb%' ORDER BY SCHEMA_NAME='testdb' DESC limit 1
2022-01-27T15:25:46.279462Z 62 Close stmt
2022-01-27T15:25:46.284871Z 62 Prepare SELECT column_name, is_nullable, data_type, character_maximum_length, numeric_precision, numeric_scale , datetime_precision FROM information_schema.columns WHERE table_schema = ? AND table_name = ? ORDER BY ORDINAL_POSITION
2022-01-27T15:25:46.284923Z 62 Execute SELECT column_name, is_nullable, data_type, character_maximum_length, numeric_precision, numeric_scale , datetime_precision FROM information_schema.columns WHERE table_schema = 'testdb' AND table_name = 'users' ORDER BY ORDINAL_POSITION
2022-01-27T15:25:46.292756Z 62 Close stmt
2022-01-27T15:26:21.872960Z 62 Query START TRANSACTION
2022-01-27T15:26:21.893559Z 62 Prepare INSERT INTO `users` (`name`,`email`,`password`) VALUES (?,?,?)
2022-01-27T15:26:21.893656Z 62 Execute INSERT INTO `users` (`name`,`email`,`password`) VALUES ('name','author','$2a$14$DuauYNQjhtLiYn1Pa7NGyupKiG2YA.lCp3Tjh84BPjAD/jADnzy8i')
2022-01-27T15:26:21.916066Z 62 Close stmt
2022-01-27T15:26:21.916193Z 62 Query COMMIT
이처럼 복잡한 SQL 쿼리문을 코드 단계에서 비교적 편하게 관리할 수 있다는 것이 직관적으로 느껴지는 ORM의 가장 큰 장점이다.
'Programming Language > Go' 카테고리의 다른 글
grpc 소개 (0) | 2022.11.15 |
---|---|
protocol 버퍼에 대한 소개 (0) | 2022.11.15 |
[go 모듈 소개 - 2. logrus] 더 자세한 로깅 모듈 (0) | 2022.11.15 |
목차
1. 쿠키란?
2. 다양한 유형의 쿠키 - 매직 쿠키 와 HTTP 쿠키
3. 쿠키는 어떤 목적으로 사용하는가
4. HTTP 쿠키의 분류 - 세션 쿠키와 퍼시스턴트 쿠키
5. 쿠키와 보안
쿠키란?
쿠키는 컴퓨터 네트워크를 사용할 때 컴퓨터를 식별하는 사용자 이름 및 암호와 같은 작은 데이터 조각이 포함된 텍스트 파일이다. 그중에서도 HTTP 쿠키는 특정 사용자를 식별하고 웹 브라우징 경험을 개선하는 데 사용된다.
쿠키에 저장된 데이터는 연결 시 서버에 의해 생성된다. 이 데이터는 유저와 유저의 컴퓨터에 고유한 ID로 레이블이 지정된다.
유저의 컴퓨터와 네트워크 서버 간에 쿠키가 교환될 때 서버는 ID를 읽고 해당 유저에게 구체적으로 어떤 정보를 제공해야 하는지를 알게 된다.
다양한 유형의 쿠키
- 매직 쿠키
- HTTP 쿠키
"매직 쿠키"는 변경 없이 송수신되는 정보 패킷을 나타내는 오래된 컴퓨팅 용어로, 일반적으로 비즈니스의 내부 네트워크와 같은 컴퓨터 데이터베이스 시스템에 로그인하는 데 사용된다. 이는 오늘날 우리가 이야기하는 최신 "쿠키"보다 더 앞선 개념이다.
"HTTP" 쿠키 또는 인터넷 쿠키는 인터넷 웹 브라우저가 각 사용자의 세션에 대한 정보를 추적, 개인화 및 저장하도록 특별히 제작되었다.
쿠키는 유저가 새로운 웹사이트를 방문할 때 해당 유저를 식별하기 위해 생성된다. 웹 사이트의 데이터를 저장하는 웹 서버는 식별을 위한 짧은 스트림을 웹 브라우저에 보낸다.
브라우저 쿠키는 "이름-값" 쌍으로 식별되고 읽는다. 서버는 웹 브라우저가 쿠키를 저장하기를 원할 때에만 쿠키를 보낸다. 웹 브라우저는 유저를 식별하는 "이름-값" 쌍을 기억하기 위해 쿠키를 로컬에 저장한다.
사용자가 나중에 해당 사이트를 다시 방문하면 웹 브라우저는 해당 데이터를 쿠키 형태로 웹 서버에 반환한다. 이때 서버는 쿠키를 통해 해당 연결에 대해서 해당 유저의 세션을 다시 가져올 수 있게 된다.
- 로그인을 통해, 인증이 완료되면 웹 사이트의 서버는 쿠키를 생성하여 브라우저에 반환한다.
- 웹사이트의 쿠키는 유저에게 제공되고 해당 유저의 웹 브라우저에 저장된다. 쿠키에는 해당 유저만을 위한 특별한 고유 ID를 가지고 있다.
- 웹 브라우저에서는 웹 사이트에 해당 유저의 쿠키를 제공하고, 웹사이트 쪽 서버에서는 쿠키의 고유 ID를 읽어 활동 데이터를 수집하고 유저가 페이지를 떠났을 때의 방문을 기억하게 된다.
쿠키는 어떤 목적으로 사용하는가
웹사이트는 HTTP 쿠키를 사용하여 웹 경험을 간소화할 수 있다. 쿠키가 없다면 사이트를 떠난 후 다시 로그인해야 하거나 실수로 페이지를 닫은 경우 장바구니에 물건을 전부 다시 담아야 할 것이다.
쿠키가 사용되는 방식은 다음과 같다.
1. 세션관리 (Session management)
예를 들어, 쿠키는 웹사이트가 사용자를 인식하고 개별 로그인 정보 및 기본 설정들을 기억할 수 있도록 한다.
2. 개인화 (Personalization)
맞춤형 광고는 쿠키를 사용하여 세션을 개인화하는 예시 중의 하나이다. 유저는 사이트의 특정 항목 또는 일부를 볼 수 있으며, 이때 쿠키는 이 데이터를 사용하여 유저에게 노출되는 타겟 광고를 구축하는데 도움을 준다.
3. 추적(Tracking)
쇼핑 사이트는 쿠키를 사용하여 사용자가 이전에 본 항목을 추적하여 사이트에서 사용자가 좋아할 만한 다른 상품을 제안하고 쇼핑을 계속하는 동안 항목을 장바구니에 보관할 수 있도록 한다.
이는 대부분 사이트를 이용하는 유저 측에서의 이익이지만, 웹 개발자 측에서도 쿠키를 사용하여 많은 것을 얻을 수 있다.
쿠키는 웹사이트 서버의 저장 공간을 확보하기 위해 장치를 로컬에 저장한다. 결과적으로 웹 사이트는 서버 유지 관리 및 저장 비용을 절약하면서 개인화할 수 있게 된다.
HTTP 쿠키의 분류 - 세션 쿠키와 영구 쿠키
세션 쿠키(Session Cookie)는 웹사이트를 탐색하는 동안에만 사용된다. 이때 쿠키는 RAM에 저장되게 된다. 하드 드라이브에는 기록되지 않는다. 세션이 종료되면 세션 쿠키는 자동으로 삭제된다.
퍼시스턴트 쿠키(Persistent Cookie)는 컴퓨터에 무기한 남아 있지만, 만료 날짜가 포함되어 있어 해당 날짜에 도달하게 되면 자동으로 제거된다.
퍼시스턴트 쿠키는 다음 두 가지 목적으로 사용된다.
1. 인증(Authentication)
이 경우 쿠키는 사용자가 어떤 이름으로 로그인했는지 여부를 추적한다. 또한 로그인 정보를 간소화하여 사용자가 사이트 암호를 기억할 필요가 없게 된다.
2. 추적(Tracking)
이 경우 쿠키는 시간이 지남에 따라 동일한 사이트에 대한 여러 방문을 추적한다. 예를 들어 일부 온라인 판매자는 쿠키를 사용하여 조회한 페이지 및 제품을 포함하여 특정 사용자의 방문을 추적한다. 그들이 얻은 정보를 통해 방문자가 관심을 가질 다른 항목들을 제안할 수 있다. 판매자는 해당 사이트에서 사용자의 검색 기록을 기반으로 방문자의 프로필을 작성하고 이를 통해 사용자의 관심사를 추적할 수 있게 된다.
쿠키와 보안
쿠키의 데이터는 변경되지 않으므로 쿠키 자체는 유해하지 않다. 쿠키가 바이러스나 기타 멜웨어를 포함하여 컴퓨터를 감염시킬 수는 없다. 다만 악의적인 해커가 의도적으로 중간에서 쿠키를 가로채고 이를 통해 해당 유저에 대한 액세스를 활성화할 수 있다는 점에 유의해야 한다. 이 경우 해커는 유저의 검색 기록을 추적할 수 있게 되며, 이는 직접적인 개인 정보 유출로 이어진다.
또한 특정 쿠키는 다른 쿠키보다 보안적으로 더 취약할 수 있다.
퍼스트 파티 쿠키와 서드 파티 쿠키의 비교
일부 쿠키는 출처에 따라 다른 쿠키보다 더 위협적일 수 있다.
퍼스트 파티(First-party)쿠키는 유저가 사용하는 웹사이트에서 직접 생성된다. 평판이 좋은 웹사이트나 손상되지 않은 웹사이트를 탐색하는 한 일반적으로 더 안전하다.
서드 파티(Third-party)쿠키는 일반적으로 보안에 더 취약하다. 해당 페이지의 광고에 연결되어 사용자가 현재 서핑하고 있는 웹 페이지가 아닌 다른 웹사이트에서 생성된다. 10개의 광고가 있는 사이트를 방문하면 사용자가 해당 광고를 클릭하지 않더라도 10개의 쿠키가 생성될 수 있다. 서드 파티 쿠키를 사용하면 광고주 또는 분석회사가 광고가 포함된 모든 사이트의 웹에서 개인의 검색 기록을 추적할 수 있게 된다.
좀비 쿠키는 타사에서 제공하며 쿠키 설치를 선택하지 않은 경우에도 사용자의 컴퓨터에 영구적으로 설치된다. 좀비 쿠키는 삭제 시에도 다시 나타난다. 좀비 쿠키는 Adobe Flash 저장소에 저장된 데이터에서 처음 생성되어 "Flash 쿠키"라고도 하며, 제거하기가 상당히 어렵다.
다른 서드 파티 쿠키와 마찬가지로 좀비 쿠키는 웹 분석 회사에서 고유한 개인의 검색 기록을 추적하는 데 사용할 수 있다. 웹사이트는 좀비를 사용하여 특정 사용자를 차단할 수도 있다.
Chapter Objective
- OS로부터 제공되는 service 란 무엇이고 어떤 종류가 존재하는가
- system call 이 어떤 방식으로 OS의 service 를 제공하는가
- OS의 다양한 Design에는 무엇이 있는지 (monolithic, layered, microkernel, modular , hybrid)
- OS에서 부팅 시 어떤 과정을 거치게 되는가
- Linux 커널과 상호작용하기 위한 Kernel module 의 디자인 모델들의 유형과 이때 구현이 어떻게 이뤄지는지
2.1 Operating-System Services
OS는 사용자에게 컴퓨터의 프로그램을 쉽고 효율적으로 실행할 수 있는 환경을 제공한다.
일종의 제어 프로그램으로서 사용자 프로그램의 오류나 잘못된 자원 사용을 감시함과 동시에 입출력 장치 등의 자원에 대한 연산과 제어를 관리하게 된다.
OS가 제공하는 서비스는 크게 User 의 편의를 위한 서비스와 System을 위한 서비스로 나뉘게 된다.
먼저 User를 위한 서비스는 다음과 같다.
1. User Interface
OS에서는 사용자와의 상호작용을 위해 GUI(Graphical User Interface)와 CLI(Command Line Interface)를 제공한다. 동시에 mobile에서는 touch interface를 통해 키보드를 통한 입력을 대체하게 된다.
2. Program execution
Loading: 프로그램의 실행 요청이 들어오는 경우, ROM에서 해당하는 코드나 프로그램 실행 시에 필요한 데이터를 RAM(메인 메모리)으로 가져와 프로그램을 실행한다.
3. I/O operation
프로그램 실행 시에 요구되는 I/O 요청에 대해서도 이를 OS가 다루게 된다. 정확히는 이를 OS 내부의 커널에서 다루게 되며, 이때 효율성(I/O operation을 이후 등장하는 개념인 API로 묶어버림)과 보안의 측면에서 user는 I/O operating을 다룰 수 없게 된다.
4. File-system manipulation
프로그램의 실행 과정에서 파일을 읽고 쓰는 과정이 수반될 수 있다. OS에서 이를 지원한다. 또한 OS를 통해 이름을 주고 이에 따라 파일을 생성(create)하거나 지우고(delete), 검색(search)하거나 나열(list)할 수 있다. file ownership을 통한 파일에 소유권에 대한 권한 관리(permission management)도 지원한다.
5. Communication
프로그램 실행 시 발생하는 프로세스들은 서로 간의 통신이 필요한 경우가 존재한다. 이를 지원하는 방식으로는 크게 두 가지가 존재한다.
- shared memory: 메모리 영역 중 공유가 허용되는 부분을 서로 다른 프로세스가 같이 사용하는 방식
- message passing : predefined format을 준수하는 패킷을 통해 정보를 전달하는 방식
6. Error detection
OS는 CPU, 메모리, I/O 기기들과 같은 하드웨어 부품들 뿐만 아니라 user program에서 발생할 수 있는 오버 플로우나 Segmentation fault, Bus error등과 같은 컴퓨터 사용 전반에 있어 발생할 수 있는 에러를 전반적으로 관리한다.
지금까지는 User 의 편의를 위한 서비스였다면 이후에 등장하는 서비스들은 System을 위한 서비스 목록에 대한 내용이다.
7. Resource allocation
자원 할당의 경우, CPU가 제공하는 대표적인 서비스 중의 하나이다. 프로그램이 실행되기 위해서는 각 하드웨어 자원의 할당이 불가피하다.
자원 할당의 예시로 CPU-scheduling routine가 있다. 이를 통해 OS는 CPU를 사용하는 최적의 방법을 통해 CPU 자원을 할당하게 된다. 이 외에도 printer나 USB 등을 관리하기 위해 기기들마다의 별도의 루틴이 존재하게 된다.
8. Logging
OS를 통해 프로그램이 어떻게 얼마나 컴퓨터의 자원을 사용했는지를 기록하게 된다.
시스템 로그는 단순히 기록을 쌓아두는 용도로 사용되기도 하며 혹은 Accounting(대표적으로 윈도우의 작업 관리자)에 사용되기도 한다.
9. Protection and security
OS는 컴퓨터 자원의 Protection과 Security적인 부분을 지원한다.
2.2 User and Operating-System Interface
유저가 컴퓨터 시스템과 상호 작용하는 방법에는 Command Interpreter, GUI(Graphical User Interface) 두 가지가 존재한다.
Command Interpreter
먼저, CLI는 잘 알려진 것처럼, 대표적으로 BASH shell, 윈도우의 명령 프롬프트 외에도 C shell, Korn shell 등이 역사적으로 사용되어 왔다.
쉘의 주요한 목적으로 파일의 create, delete, list, print, copy, excute등이 있는데, 두가지 방법으로 쉘을 구현할 수 있다.
- Command Interpreter 내부에 실행 가능한 명령어 세트를 포함하는 방식
- system program(/bin 하위의 명령어 파일)을 실행하는 방식: 새로운 명령어를 쉽게 추가할 수 있고, 이때마다 쉘을 변경할 필요가 없다.
GUI
GUI를 통해 더욱 시각적인 방법을 통해서도 컴퓨터와 상호작용할 수 있다.
윈도우의 Desktop에서는 파일의 icon이나 folder를 통해서 Command Interpreter에서 명령어를 입력하며 수행했던 작업들을 수행할 수 있다.
2.3 System Calls
시스템 콜을 이해하기 위해서는 1장에서 배웠던 Interrupt라는 개념을 다시 살펴보자
Interrupt를 크게 살펴보면 다음과 같다.
Interrupt라는 큰 범주 아래 다음과 같이 먼저 분류가 된다.
- (H/W) Interrupt -> 1장에서 언급이 되었던 Interrupt
- (S/W) Trap
이 중에서 우리가 지난 번 1장에서 살펴보았던 Interrupt가 바로 (H/W) interrupt를 의미한다. 우리가 흔히 interrupt라고 부르는 것들은 대부분이 하드웨어에 대한 interrupt를 의미하게 된다.
이 외에도 소프트웨어적으로 CPU에게 보내는 신호가 존재한다. 이를 Trap이라고 한다. Trap 또한 interrupt의 일종으로 software interrupt라고도 불린다. 이 중에서 Trap을 다시 두 개로 분류한다. 바로 Exception과 System call이다.
Exception은 우리가 자주 마주하게 되는 Segmentation Fault, Bus Error, Overflow 등등과 같은 컴퓨터 시스템을 사용하면서 발생하는 에러를 처리하는 Trap이다.
우리가 알아보고자 하는 Trap은 Exception과 함께 Trap의 일종으로 커널의 서비스(대표적으로 ISR로 인한 함수 calling)나 유저 애플리케이션의 실행 시 발생하는 하드웨어 자원 요청 등이 일어났을 때 발생하는 신호로 이해할 수 있다.
User Application의 System Call이 일어나는 과정은 다음과 같다.
- 유저 프로세스의 실행 (이때 mode bit 1)
- 하드웨어 자원을 요청하기 위한, system call
- mode bit를 0으로 바꾸고, system call을 적절하게 처리
- mode bit를 다시 1로 바꾸고, 시스템 콜 결과를 user process의 리턴
이때, System call을 일으키기 위해 유저는 앞서 OS에서 미리 정의된 API를 활용하게 된다. API는 곧 다시 함수들을 라이브러리 형식으로 모아둔 형태로 동작한다. 책에서 등장한 libc가 대표적이다. 이렇게 API로 커널 영역과 User 영역을 분리함으로써, 애플리케이션 프로그래머는 커널 이하 부분의 구현을 이해하지 않아도 되고, 커널은 외부에서 수정, 편집이 불가능하여 보안적으로 이점을 갖게 된다.
또한 직접 시스템 호출 대신 API를 사용하면 서로 다른 시스템 간의 프로그램 이식성이 향상된다. API는 시스템 호출 인터페이스를 통해 적절한 시스템 호출을 수행하게 되며, 테이블을 룩업함으로써 특정 번호가 매겨진 시스템 호출이 이뤄진다.
Interrupt만큼이나 System Call도 상당히 많이 이뤄진다는 점을 기억해야 한다. 하드웨어의 접근이 발생할 때마다 그에 따라 당연히 System Call이 발생한다고 생각하면 그 빈도를 유추해볼 수 있다.
System Call이 가능하게 하는 요소로 RTE라는 것이 등장한다. RTE는 프로그램의 실행에 관여하는 Library와 Loader등을 포함한다. 중요하게 봐야할 점으로 RTE는 system-call interface를 포함하고 있다는 점이다.
System Call Interface는 앞서 1장에서 살펴보았던 Interrupt Vector Table의 System Call 버전이라고 생각하면 되겠다.
해당 과정을 더 자세히 살펴보자
1. 유저 프로그램이 시스템 콜(System Call)을 호출한다. my code에서 printf()는 I/O instruction과 관련된 함수이다. 해당 함수는 library에 있는 함수이고 실제 동작할 때 library는 I/O를 하기 위해 시스템 콜을 호출할 것이다. 이는 커널에게 I/O instruction의 수행을 위임한다는 의미이다.
2. 유저는 CPU 제어권을 뺏기고 더 이상 user mode에서 실행할 수 없게 되는 Trap에 걸린다. Wrapper Routine은 Trap으로 넘어갈 내용을 준비하고 실질적으로 Trap을 일으키는 공간이다. Trap을 일으키기 전 Prepare parameter들을 준비하는데 이 중에서 가장 중요한 것이 아래 나올 System call number이다.
* 커널은 System call function의 시작 주소를 담고 있는 배열을 가지고 있다. 이 배열이 System call table이고 배열의 index 번호가 System call number이다. 예를 들어 System call number가 4이면 커널에서 write() 함수를 불러 작업을 수행한다는 뜻이다.
3. 하드웨어가 유저 모드에서 커널 모드로 모드 비트(mode bit)를 바꾼다. chmodk(change CPU mode to Kernel) 명령을 통해 CPU의 비트 모드가 유저 모드에서 커널 모드로 변경되게 된다. chmodk가 실행되면서 프로그램은 런타임 중 Trap에 걸려 커널 영역으로 가게 된다.
4. 하드웨어가 sys_call() 이라는 Trap Handler로 가게 되고, Trap Handler는 커널 안의 assembly function을 수행한다.
*커널 안의 모든 System call function의 이름은 sys_로 시작한다.
5. 지금까지 유저 프로그램에서 진행했던 단계를 저장한다. 커널 쪽 일이 다 끝나면 시스템 콜을 호출했던 곳으로 돌아가서 다시 진행을 해야하기 때문에 저장을 한다.
6. System call number가 커널 안에 System call table에 있는 번호에 맞는 번호인지 확인한다. 예를 들어 Wrapper Routine에서 설정한 write() 함수의 시작 주소를 뜻하는 index 번호, 즉 System call number가 4번 인 것과 실제 커널 안의 System call table에서 write() 함수의 시작 주소가 있는 index 번호가 일치하는지 비교한다.
7. 맞는 번호라면 System call function의 주소를 가져온 후 작업을 수행한다.
8. 커널 쪽 일이 다 끝나면 다시 시스템 콜을 호출했던 유저의 영역으로 돌아가고 모드 비트를 커널 모드에서 유저 모드로 다시 전환한다.
IDT
- 시스템 콜을 직접적으로 호출하는 대신 간접적으로 IDT(Interrupt Descriptor Table)를 거쳐서 해당 call이 System Call임을 명시
- fork는 라이브러리 함수이고, 이에 대해서 system call num을 명시(INT $0x80)하는 형태로, 이후 시스템 콜 테이블에 따라 유저 스페이스에서 커널 스페이스로의 접근이 가능
- IDT에서 0x80이므로 해당 Call이 System Call(software interrupt)임을 인지하고, SYMBOL_NAME에 따라서 해당하는 system call table로 이동
- 시스템 콜은 테이블 형태(sys_call table)로 정의되어 있다. 이를 system call interface라고도 한다.
*IDT는 User 영역과 Kernel 영역을 구분해준다는 점을 주목하자.
System call이 이뤄질 때는 단순히 API의 호출로만 이뤄지지 않는다. 호출과 동시에 다양한 매개변수를 전달할 필요가 있다. 이때 매개변수를 넘기는 방법에는 총 세가지 방법이 존재한다.
- Parameter를 레지스터에 직접 넘긴다.
- Table, Block으로 여러 정보를 Wrap해서 해당하는 주소를 넘긴다.
- 위의 두 방식을 혼합한다(Linux에서 채택, *매개 변수가 5개를 넘어가면, 즉 6개 이상부터는 2번을 사용한다. 이 외에는 1번을 채택, 매개변수의 개수를 조절할 필요가 있는 이유)
Types of System Calls
System Call를 다음 6개의 카테고리로 분류할 수 있다.
- process control
- file management
- device management
- information maintenance
- communication
- protection
process control
(1) 끝내기(end), 중지(abort)
- 실행 중인 프로그램은 수행을 정상적으로(end()), 혹은 비정상적으로(abort()) 종료할 수 있어야 한다.
(2) 적재(load), 수행(execute)
- 한 프로그램을 실행하고 있는 process나 job이 다른 프로그램을 load 하고, execute 하기를 원할 수 있다.
- load된 프로그램이 종료 되었을때, 어디로 Control을 Return해야 하는가에 대한 질문이 생길 수 있다.
- 만약 Control이 기존 프로그램으로 돌아간다면, 기존 프로그램의 Memory image를 save 해놔야 한다.
- 만약 두 프로그램이 병행하게 수행된다면, Multi-programming 될 새로운 job이나 process를 생성한 것이다.
(3) 프로세스 생성, 프로세스 종료
- new job, process 혹은 set을 생성한다면, 이들의 execution을 control 할 수 있어야 한다.
(4) 프로세스 속성 획득, 속성 결정
- job의 priority, maximum allowable execution time 등을 포함하여 속성을 결정할 수 있어야 한다.
(5) 시간 기다리기(wait for time)
- new job이나 process를 생성한 후에는, 이들의 실행을 끝나기를 기다릴 필요성이 있을 수 있다.
(6) wait event, signal event
- 특정 사건이 일어날 때까지 기다릴 수 있을 수 있어야 한다.(wait event)
- 그 사건이 일어나면 신호를 보낼 수 있을 수 있어야 한다. (Signal event)
(7) Allocate and free memory
- 둘 이상의 프로세스들이 공유하는 데이터들에 대해 일관성을 보장해야 할 필요성이 있을 수 있다.
- 이에 운영체제는 종종 프로세스가 공유 데이터를 lock 할 수 있는 System Call을 제공한다.
File Management
(1) 파일 생성(Create File), 파일 삭제(Delete File)
- 이 System Call은 보통, 파일 이름이나 파일 속성의 일부를 요구한다.
(2) 열기 (Open), 닫기(Close)
- 생성된 파일들을 사용하기 위해서는 열기(Open) System call을 사용한다.
- 파일을 더 이상 사용하지 않을 때는, 파일 닫기(Close) System Call을 사용한다.
(3) 읽기, 쓰기, 위치 변경(Reposition)
- 파일을 열은 뒤에, 읽기(Read()), 쓰기(Write())를 할 수 있다.
- 위치 변경(Reposition(), (ex) 파일의 끝으로 건너뛰기)를 할 수 있다.
(4) 파일 속성 획득 및 결정 (get file attributes, set file attributes)
- 파일 속성에 대한 정보를 얻을 수 있고, 변경할 수 있다.
Device Management
- 프로세스는 작업을 계속 수행하기 위해 Main Memory, Disk Drive 등의 추가 자원을 필요로 할 수 있다.
(1) 장치를 요구(Request Devices), 장치를 방출(Release Devices)
- 다수의 사용자가 동시에 사용하는 시스템은 독점적인 device 사용을 보장받기 위해 Request 해야 한다.
- device의 사용이 끝나면, 반드시 Release 해야 한다.
(2) 읽기, 쓰기, 위치 변경(Reposition)
- 일단 device를 request 하여 Allocated 되면, 해당 device를 Read, Write, Reposition 할 수 있다.
- I/O 장치와 파일들 간에는 유사성이 많기 때문에, UNIX를 포함한 많은 운영체제가 이들 둘을 통합한 파일-장치 구조(file device structure)로 결합하였다.
(3) 장치 속성 획득, 장치 속성 결정 (get device attributes, set device attributes)
(4) 장치의 논리적 부착 또는 분리 (Logically attach or detach devices)
몇몇 OS에서는 File system과 Device management를 동일 선상에 두고 처리하는 경우도 많다. 예를 들어 Device를 관리할 경우 이를 특수한 형태의 file name으로 인지하는 경우가 존재한다.
Information Maintenance
(1) 시간과 날짜의 설정과 획득 (get time or date, set time or date)
- 대부분의 시스템은 현재 시간(time()), 날짜(date())를 되돌려 주는 System Call을 가지고 있다.
(2) 시스템 데이터의 설정과 획득 (get System data, set System data)
- 현재 사용자 수, OS의 Version number, Free Memory와 같은 시스템에 관한 정보를 알려주기도 한다.
(3) 프로세스, 파일, 장치 속성의 획득 (get process, file, or device attributes)
- 운영체제는 현재 실행되고 있는 프로세스들에 관한 정보를 가지고 있다.
(4) 프로세스, 파일, 장치 속성의 설정 (set process, file, or device attributes)
- 운영체제는 현재 실행되고 있는 프로세스들에 관한 정보에 접근하여 재설정할 수 있다.
Communication
- Communication에는 Message Passing과 Shared Memory model이 있다.
1. Message Passing Model
- 통신하는 두 process가 정보를 교환하기 위해 서로 메시지를 주고받는다.
- Message는 두 프로세스 사이에서 직접 교환되거나, mailbox를 통해 간접적으로 교환될 수 있다.
2. Shared Memory Model
- process는 다른 process가 소유한 Memory 영역에 대한 접근을 하기 위해 System call을 사용한다.
- 정상적으로, OS는 한 process가 다른 process의 memory를 접근하는 것을 막으려고 한다.
- 이에, Shared Memory Model에서는 이런 제한을 제거해야 할 필요성이 있다.
- 대부분의 System은 두 가지(Message Passing, Shared Memory) 모두를 구현한다.
- Message Passing은 Conflict가 없기 때문에, 소량의 데이터를 교환하는데 좋다.
- SHared Memory는 최대 속도와 편리한 통신을 할 수 있지만, 동기화 부분에서 문제점이 있다.
(1) 통신 연결의 생성, 제거 (create, delete communication connection)
(2) 메시지의 송신, 수신 (Send, Receive Messages)
(3) 상태 정보 전달 (Transfer status information)
(4) 원격 장치의 부착(Attach) 및 분리(Detach)
Protection
- 과거에, Protection은 다수의 사용자를 가지는 다중 프로그램 시스템에서만 고려되는 문제였다.
- Networking과 Internet의 출현으로, 서버에서 휴대용 컴퓨터까지 모든 컴퓨터 시스템에서 고려해야 한다.
2.4 System Services
- 시스템 프로그램은 시스템 유틸리티(System Utility)로 알려져 있으며, 프로그램 개발과 실행을 위해 보다 편리한 환경을 제공한다.
- 시스템 프로그램(System Program)은 파일 관리, 상태 정보, 파일 변경, 프로그래밍 언어 지원, 프로그램 적재와 수행, 통신, 백그라운드 서비스의 범주로 분류할 수 있다.
- 대부분의 운영체제는 시스템 프로그램과 함께 일반적인 문제점을 해결하거나, 일반적인 연산을 수행하는데 유용한 프로그램들도 제공한다.
- 이러한 Application Program에는 Web Browser, Word Processor, DataBase System 등이 포함된다.
2.5 Linkers and Loaders
링커와 로더는 많은 부분이 연관되어 수행되지만 다른 작업들을 수행한다.
- 프로그램 로딩: 이것은 프로그램을 실행가능한 상태로 만들기 위해 Disk로부터 프로그램 이미지를 읽어서 메인 메모리로 복사하는 것을 말한다.
- Relocation: 컴파일러와 어셈블러는 각각의 입력 파일들로부터 시작 주소가 제로인 오브젝트 코드를 생성한다. Relocation이라는 것은 프로그램의 각기 다른 부분들(코드와 데이터)에 대해 로드되는 주소를 할당하는 것이다. 이러한 작업은 같은 타입(코드 혹은 데이터)으로 정의된 모든 구간들을 하나의 구간으로 합치고, 이러한 구간들이 런타임때 올바른 주소를 가리킬 수 있도록 조정하는 것을 말한다.
- 심볼 해석: 프로그램은 다양한 하위 프로그램들도 구성된다. 하나의 상위 프로그램이 다른 하위 프로그램을 참조하는 것은 심볼이라는 것을 통해 이뤄진다. 링커의 작업은 이러한 심볼의 위치를 알아내어 상위 프로그램의 오브젝트 코드에 하위 프로그램의 주소를 기입하여 참조를 해석하도록 한다.
참고) 오브젝트 파일
오브젝트 파일이란 오브젝트 코드로 구성된 파일을 말한다. 컴파일러는 컴파일 과정에서 C언어나 Java와 같은 High level언어로 구성된 소스코드를 기계어나 어셈블리어로 변환시킨다. 이 같은 과정을 통해 변환된 기계어를 컴퓨터가 이해할 수 있는 언어, “Object code” 또는 “목적 코드"라고 부른다.
하지만 이렇게 만들어진 오브젝트 파일은 실행가능하지 않다.
실행 가능한 오브젝트 파일을 만들기 위해서는 링커의 역할이 필요하다.
일반적인 재배치 가능한 오브젝트 파일은 내부에 심볼 테이블을 가지고 있다. 심볼 테이블에는 소스코드에서 참조되는 심볼들의 이름과 주소가 정의되어 있다.
하지만 오브젝트 파일의 심볼 테이블에는 해당 오브젝트 파일의 심볼 정보만을 가지고 있어야 하기 때문에, 다른 파일에서 참조되는 심볼들의 이름과 주소가 정의되어 있다.
링커는 오브젝트 파일에 있는 데이터의 주소나 코드, 심볼들을 참조하고, 불완전한 데이터, 코드와 심볼들을 묶어 연결하고 여러 소스파일 간에 심볼들을 공유하여 기계어로 된 실행 파일인 “실행가능한 오브젝트 파일"을 생성한다.
링커가 하는 일은 크게 2 가지가 있다.
1. Relocation
오브젝트 파일에 있는 데이터의 주소나 코드 메모리 참조 주소를 알맞게 재배치한다.
2. Symbol Resolution
오브젝트 파일에 같은 이름의 심볼이 존재하고 있을 때, 어떤 심볼을 사용할 것인지 결정한다.
오브젝트 파일은 3가지 종류가 존재한다.
Relocatable object file
하나의 파일을 컴파일하였을 때 나오는 일반적인 오브젝트 파일이다. 실행가능한 오브젝트 파일을 만들기 위한 재배치 가능한 심볼 정보(symbol table)들을 가지고 있다. 실행 가능한 오브젝트 파일을 만들기 위해 다른 relocatable file들과 결합될 수 있다.
Executable Object file
링커가 여러 개의 오브젝트 파일들을 연결시켜서 만들어낸 실행 가능한 오브젝트 파일이다. 직접 메모리에 로드, 다운하여 실행될 수 있는 프로그램 파일이다. 메모리로 직접 로드되어 실행될 수 있는 것을 말한다.
Shared Object file
재배치 가능한 오브젝트 파일의 특별한 타입 , 동적인 컨텍스트들을 링크할 수 있는 코드와 데이터를 가지는 파일이다.
참고) ELF란
링커는 오브젝트 파일에 있는 주소나 코드, 심볼들을 참조하고, 불완전한 데이터와 코드, 심볼들을 묶어 연결하고, 여러 소스 파일 간에 이를 공유하여 기계어로 된 실행파일인 “Executable Object File”을 생성한다. 이와 같은, 링크 과정을 위해서는 오브젝트 파일이 그 자체로써 링커에게 정보를 제공할 수 있어야 한다.
모든 파일이 일정한 포맷 형식을 가진다면 어떤 오브젝트 파일이더라도 링커는 정보를 쉽게 읽어올 수 있다. 리눅스와 같은 유닉스 시스템들은 파일 포맷으로 ELF, Executable and Linkable File format 형식을 따른다.
실행 가능한 오브젝트 파일(Executable Object File)의 ELF 포맷 형식은 다음과 같다. 왼쪽은 링커가 오브젝트 파일을 링크하기 전의 Linking View 이며, 오른쪽은 링크가 끝나고 난 후에 생성된 실행가능한 ELF 오브젝트 형식인 Execution View이다.
ELF에 해당하는 파일 리스트
none("예를 들어 main 이라는 실행 가능한 파일"), .axf,
.bin, .elf, .o,
.out, .prx, .puff,
.ko, .mod, and .so
Section과 Segment
Section은 타겟 PC에 프로그램을 다운로드하기 쉽도록 데이터의 유형에 따라 자세하게 나눠놓은 것이라고 생각하면 되며, Segment는 그러한 Section들의 집합이라고 생각하면 된다.
참고로, 메모리 구조에서 메모리를 크기가 서로 다른 논리적 단위인 세그먼트(Segment)로 나누는 방식을 “세그먼트 기법”이라고 한다. 메모리를 일괄적으로 같은 크기로 나누는 것이 아니라, 데이터의 용도와 크기에 따라 나누는 방식이다.
각각의 구성 요소는 다음과 같이 구성되어 있다
노션 정리 링크: https://vigorous-quit-f7e.notion.site/ELF-Section-3088cc9f84094d5cac78bdf04cc3c822
추가로 컴파일러에 따라서 데이터를 RW, ZI, RO 세가지 형태로도 구분하는데
- RW: Read-Write, 초기값이 설정되어 있는 전역변수, static 변수
- .data Section과 유형의 속성이 같다.
- ZI: Zero-Initialized, 초기값이 0인 전역 변수 또는 프로그램 실행시에 0으로 초기화되는 전역 변수, 그리고 프로그램에 의해 Stack, Heap 메모리에 공간을 할당받는 지역변수
- .bss Section과 유형의 속성이 같다
- RO: Read-Only, 수정이 불가능한 코드, const 전역변수
- .text, .rodata Section과 유형의 속성이 같다.
더 나아가, RAM과 ROM 메모리가 모두 있는 타겟 시스템에서 RW, ZI, RO 세가지 형태의 데이터가 어떤 메모리에 저장되는 것이 효율적인지 알아보자.
- RW는 ‘초기값’을 가지고 있는 데이터 이므로 ROM 메모리에 저장하되, ‘변수'로써 사용하기 위해 프로그램 실행시에 수행되는 스타트업 코드(Startup Code)에서 RAM 메모리로 불러와 사용한다.
- ZI는 초기값으로 0을 가지고 있는 변수들이므로 굳이 ROM에 저장하지 안하도 된다. 스타트업 코드를 수행하면서 0으로 초기화하여 RAM 메모리로 불러와 사용한다.
- RO는 변하지 않아야 하는 데이터이므로, 시스템의 상태에 관계없이 데이터가 유지될 수 있도록 ROM에 저장한다. ROM에 직접 접근하여 데이터를 읽거나, RAM으로 복사하여 사용한다.
2.6 Why Applications Are Operating-System Specific 77
2.7 Operating-System Design and Implementation 79
- 설계 목표 (Design Goals)
- 요구 조건은 근본적으로 사용자 목적과 시스템 목적의 두 가지 기본 그룹으로 나눌 수 있다.
- 사용자 입장에서, 시스템은 사용하기에 convenient to use, easy to learn, Reliable, safe, fast 해야 한다.
- 시스템적 입장에서, 운영체제는 설계, 구현, 유지 보수가 쉬워야 하며, 적응성, 신뢰성, 효율성 등을 가져야 한다.
- 기법과 정책 (Mechanisms and Policies)
- 기법(Mechanism)은 how to do something(어떻게 할 것인가)을 결정하는 것이다.
- 정책(Policy)는 what will be done(무엇을 할 것인가)를 결정하는 것이다.
- 구현 (Implementation)
- 초창기 운영체제는 어셈블리어로 작성되었으며, 현재 대부분의 운영체제는 고급 언어로 작성된다.
- 운영체제를 고급 언어로 구현하는 것에 대한 단점은 속도가 느리고, 저장 장치가 많이 소요되는 것이다.
- 하지만, 고급언어를 통한 sophisticated optimization을 수행하고, 좋은 자료구조와 알고리즘을 사용함으로써 위의 문제를 해결하고, 성능 향상을 이룰 수 있다.
2.8 Operating-System Structure 81
Monolith Structure
복잡하고 모듈화된 OS구조의 반대 개념이다.
어플리케이션 및 모든 커널 서비스들이 같은 주소 공간에 위치하기 때문에, 각 컴포넌트 간의 커뮤니케이션이 비교적 효율적이다.
하지만 어느 한 부분이 수정되면 전체를 다시 컴파일 해야하고, 한 컴포넌트에서 발생한 문제가 전제 시스템의 문제가 될 가능성이 크다.
- 모듈로 나눠지지 않는다.
- 인터페이스와 기능 수준이 잘 구분되어있지 않다.
- ex) MS-DOS
Layered Approach
Monolithic의 단점을 보완할 수 있는 계층 구조이다.
상위 계층은 하위 계층을 호출되고, 하위 계층은 상위 계층에게 호출되어 정보를 제공한다.
각 계층별로 구분되어 있기 때문에 Monolithic 구조에 비해 어느 한 부분에 발생한 문제를 해결하기 비교적 쉽다. (그 계층에 대해서만 수정 및 재컴파일 하면 되기 때문)
하지만, 개념적으로 어디서부터 어디까지를 한 계층으로 둘 것인지 정의하는 것이 애매하고, 계층 간 정보를 주고받는 과정에서 발생하는 오버헤드로 효율은 비교적 낮은 편이다.
- 운영체제를 여러 계층으로 나누는 방식
- 상위 계층은 하위 계층의 인터페이스를 이용
- 최하위 계층 : 하드웨어. 최상위 계층 : 유저 인터페이스
- 장점 : 인터페이스만 유지하면, 하위계층은 상위계층과 무관하게 수정이 가능하다.
- 단점 : 근데 계층구분이 어렵다.
Microkernel
단일 구조인 UNIX가 점점 확장되면서, 커널이 점점 더 커지고 관리하기가 어려워지기 시작했다.
이에 따라 커널에서 중요하지 않은 구성 요소는 제거하여 사용자 수준 프로그램으로 구현한 마이크로커널 형태의 접근이 생겨났다. 커널의 핵심 기능들만 유지시키고, 다른 기능들은 User-mode에서 동작하도록 상위 계층으로 올려 구현한다.
위 그림에서 CPU 스케줄링, 메모리 관리 등의 핵심 기능은 마이크로커널 내에 유지되고, 파일 시스템, 장치 드라이버 등의 기능은 User-mode 프로그램으로 올라간 것을 볼 수 있다. 이로써 커널의 규모를 축소시킨 구조이다.
커널의 확장성과 이식성이 증대하지만, 성능에 대한 영향이 단점으로 작용할 수 있다.
- 커널에 꼭 필요한 기능만 가지도록 구성된 커널
- 장점 : 확장이 용이, 높은 안정성, 높은 신뢰성, 높은 이식성
Modules
단일 구조인 UNIX가 점점 확장되면서, 커널이 점점 더 커지고 관리하기가 어려워지기 시작했다.
이에 따라 커널에서 중요하지 않은 구성 요소는 제거하여 사용자 수준 프로그램으로 구현한 마이크로커널 형태의 접근이 생겨났다. 커널의 핵심 기능들만 유지시키고, 다른 기능들은 User-mode에서 동작하도록 상위 계층으로 올려 구현한다.
위 그림에서 CPU 스케줄링, 메모리 관리 등의 핵심 기능은 마이크로커널 내에 유지되고, 파일 시스템, 장치 드라이버 등의 기능은 User-mode 프로그램으로 올라간 것을 볼 수 있다. 이로써 커널의 규모를 축소시킨 구조이다.
커널의 확장성과 이식성이 증대하지만, 성능에 대한 영향이 단점으로 작용할 수 있다.
대부분의 현대 운영체제(Linux, Mac OS X, Windows)는 LKM(Loadable Kernel Modules)를 구현한다.
Hybrid Systems
대부분의 실제 운영 체제는 위에서 설명한 운영 체제 설계 모델의 순수한 예가 아니다. 실제 시스템은 일반적으로 서로 다른 설계 모델이 혼합되어 있다.
mac OS, iOS system은 darwin으로 알려진 커널 환경을 가지고 있다. 다윈은 다양한 설계가 혼합된 하이브리드 시스템의 한 예이다. mach 마이크로커널과 mach 마이크로커널을 통해 구현된 BSD 유닉스 커널을 가지고 있다. 프로그램은 mach system call을 할 수 있고, BSD system call을 할 수도 있다.
Android 시스템 내부에 일부 수정된 Linux 커널이 존재한다.
Window Subsystem for Linux
다른 운영 체제를 에뮬레이트하는 일부 서브시스템을 가지고 있다. 예를 들어 윈도우즈 시스템에서 기본 리눅스 응용 프로그램을 실행할 경우 이를 가능하게 하는 Linux를 위한 subsystem이 존재한다. 리눅스 환경에서 실행 중인 응용 프로그램이 시스템 호출을 할 때 기본적으로 윈도우즈 시스템 호출로 이를 변환한다.
2.9 Building and Booting an Operating System 92
운영체제 생성(Operating-System Generation)
- 운영체제는 다양한 주변 구성을 가진, 여러 부류의 기계에서 수행되도록 설계되는 것이 일반적이다.
- 이 경우, 시스템은 각 specific computer site를 위해 구성되거나 또는 생성되어야 한다.
- 이 절차를 시스템 생성(SYSGEN)이라고 한다.
시스템 부트(System Boot)
- 운영체제가 생성된 이후에는, 하드웨어에 의해 사용 가능해야 한다.
- 대부분의 컴퓨터 시스템에서는 Bootstrap Program이 커널을 찾은 뒤, Main Memory에 load 하고,
수행을 시작한다.
- PC와 같은 일부 컴퓨터 시스템은 단순한 Bootstrap Program이 좀 더 복잡한 Bootstrap Program을
디스크로부터 적재하고, 이 Bootstrap Program이 다시 커널을 load 하는 2단계 절차를 사용한다.
- 컴퓨터가 전원을 켜거나, 재부팅 등의 사건을 받으면 명령 레지스터는 미리 지정된 메모리 위치를 가리키고,
그곳에서부터 실행을 시작한다. (해당 위치에는 최초의 Bootstrap Program이 존재한다.)
- RAM은 시스템 시작 시에 알 수 없는 상태가 되기 때문에, Bootstrap Program은 ROM 안에 저장된다.
- bootstrap program은 먼저, 기계의 상태를 진단하여 이상이 없는지 확인한다.
- 이후, CPU Register, device Controller, Main Memory 등 시스템 전반에 걸쳐 초기화한다.
- 위의 작업이 끝나면, bootstrap program은 운영체제를 시작한다.
- 휴대전화, 태블릿 등의 시스템들은 운영체제의 크기가 작은 경우가 많아서 운영체제 전체를 ROM에 저장한다.
- 이 방식은 bootstrap code가 변경되면, ROM을 변경해야 한다는 점이다.
- 이 문제를 해결하기 위해 쓰기가 가능한 EPROM을 사용할 수 있다.
컴퓨터를 켜면 부트스트랩 프로그램(Bootstrap program)이라는 초기화 프로그램이 실행된다. 이 프로그램을 컴퓨터의 ROM(Read-Only Memory)이나 EEPROM(Electrically Erasable Programmable Read-Only Memory)에 저장되어 있으며, 주로 펌웨어(Firmware)라고 불린다. 부트스트랩 프로그램은 시스템을 초기화하고, 부트로더(Boot loader)를 실행한다. (멀티부팅 컴퓨터의 경우 부트로더가 여러 운영체제를 가리키고 있는데, 이 경우엔 어떤 운영체제를 실행할지 선택해야 한다.) 그리고 부트로더는 최종적으로 운영체제를 실행하게 된다.
부트 스트랩의 경우 BIOS, UEFI, GRUP 등이 책에 등장한다. 먼저, BIOS의 경우 컴퓨터가 실행되면 실행되는 initial boot loader이다. 이러한 initial boot loader는 일부 초기화 작업 등을 진행한 이후 작업을 이후의 boot loader에게 넘기게 된다. 대부분의 부팅 작업은 GRUP과 같은 이후의 boot loader에서 이뤄지게 된다.
커널이 로드, 실행되면 시스템과 사용자에게 서비스를 제공해야 한다. 이때 일부 서비스는 커널 외부에서 제공되는데, 이들은 부팅할 때 메모리에 로드되는 시스템 프로세스(System processes)나 시스템 데몬(System daemons)이다. UNIX의 경우 첫 시스템 프로세스는 init이며, 이 프로세스는 또 다른 데몬들을 실행시킨다. 데몬은 프로세스로 백그라운드에서 돌면서 시스템 로그는 남기는 등의 여러 작업을 한다. 이러한 과정이 끝나면 시스템이 완전히 부팅되고, 이벤트가 발생하기를 기다리게 된다.
대부분의 OS의 경우, 부팅 과정에서 메인 메모리를 접근하는 방식이 다소 특이하다. initramfs 라는 임시 RAM file 시스템에서 작업을 진행하다가, 이후 부팅 프로세스가 완료되면 이를 Real root file system에 옮기게 된다.
2.10 Operating-System Debugging 95
- 디버깅(Debugging)란 Hardware와 Software에서의 시스템 오류를 발견하고, 수정하는 행위이다.
- 장애 분석(Failure Analysis)
- 프로세스가 실패했을 때, 운영체제는 프로세스가 사용하던 메모리를 캡처한 뒤 'core dump'를 취하고
이후의 분석을 위해 파일로 저장한다.
- 커널 장애는 Crash라고 불린다. 해당 오류 정보는 로그 파일에 저장되고, 이때의 메모리의 상태가 'Crash dump'에 저장된다.
- 성능 조정(Performance Tuning)
- 병목(Bottlenecks) 지점을 제거함으로써 성능을 향상시키려고 하는 것도 디버깅에 포함된다.
- 이에 병목 지점을 발견하기 위하여 시스템 성능을 감시할 수 있어야 하며, 시스템 동작을 측정하고 표시할 수 있는 방법을 가지고 있어야 한다. ((ex) Windows 작업 관리자.)
2.11 Summary 100
Practice Exercises 101
https://codex.cs.yale.edu/avi/os-book/OS10/practice-exercises/PDF-practice-solu-dir/2.pdf
Further Reading 101
'CS' 카테고리의 다른 글
Cache Hit, Cache Miss 개념, Cache Miss의 종류 (0) | 2022.11.28 |
---|---|
Cache Memory 소개 (0) | 2022.11.28 |
[리눅스 프로그래밍] Kernel 개요 (1) | 2022.09.23 |
[OS] Introduction (1) | 2022.09.23 |
[리눅스 프로그래밍] 리눅스 계보 (2) | 2022.09.23 |
소개글
본격적인 패턴 소개 이전에 클라우드 네이티브 애플리케이션을 지탱하는 쿠버네티스의 핵심 개념들을 설명한다. 구체적인 쿠버네티스 리소스를 통해 주로 쿠버네티스의 추상화 방식에 대해서 다루게 된다.
DDD
도메인 주도 설계(Domain Driven Design)는 이를 사용하는 사람들의 영역 또는 지식의 영역을 중심으로 하는 소프트웨어 개발 철학이다. 과거의 많은 소프트웨어 설계에서는 객체가 가져야 할 데이터에 초점을 두어 객체를 설계하였다(데이터 주도 설계, Data Driven Design). 이러한 방식은 객체 자신이 포함하는 데이터를 조작하는 데에 있어서는 좀 더 용이할지 모르나, 실제 비즈니스 레벨에서 다뤄져야 하는 내용들이 코드 상에 반영되지 못하고, 소프트웨어와 현실 간의 괴리가 발생하는 경우가 많았다. 또한 설계 시에 협력을 고려하지 않았기에, 이 후 수정이 발생하는 경우 많은 비효율이 발생한다. 해당 객체의 의미가 무엇이고 어디에 사용되는지 본인 조차 모르는 경우가 종종 발생한다.
DDD는 이러한 괴리를 해소하기는 데에 초점을 둔다. DDD에서는 해당 소프트웨어가 사용될 실제 비즈니스 영역에서부터 출발한 설계를 지향한다. 이때의 도메인은 실제 세계에서 사건이 발생하는 집합 정도로 생각할 수 있을 것 같다. 즉, DDD는 현실에서 출발한 여러 도메인들의 상호작용을 객체 지향적인 언어로 풀어낸다고 이해할 수 있을 거 같다.
바운디드 컨텍스트 (Domain Driven Design):
- DDD의 중심 패턴. 컨텍스트를 적절하게 식별 및 분리를 통해 마이크로서비스로 전환할 수 있다.
- OOP에서 하나의 객체에 넣을 기능의 범위를 고민하듯, 하나의 컨텍스트 내부에도 어느 정도의 기능을 넣을지 고민해야 한다.
- 앞서, 데이터 주도 설계가 객체들을 Public하게, 즉 누구나 접근 가능하게 열어 놓았다면 DDD에서는 Bounded Context를 통해 실제 해당 도메인 내에서만 객체들이 상호작용할 수 있도록 설계한다. 이를 서비스 관점에서 바라보게 되면 MSA를 의미하게 된다.
해당 교재에서는 가볍게 이러한 주제가 있다는 정도로 소개하고 넘어가지만, DDD는 MSA를 지탱하는 주요한 개념이기 향후 DDD를 본격적으로 공부하여 포스팅할 계획을 세우고 있다.
클라우드 네이티브의 단계
책에서는 현대 MSA 환경의 추상화 레벨을 다음과 같이 표현한다.
Level 0: 코드 레벨
변수와 메소드, 인스턴스화하기로 결정한 클래스 등을 의미한다.
Level 1: 도메인 주도 설계
실제 비즈니스에 최대한 가깝게 설계하도록 소프트웨어를 설계하는 일종의 방법론.
비즈니스 경계 (바운디드 컨텍스트), 트랜젝션 경계(애그리깃), 사용하기 쉬운 인터페이스, 풍부한 API 등이 고려된다.
Level 2: 마이크로서비스 아키텍처 방식
분산 애플리케이션 설계 원칙을 고려하여 소프트웨어 아키텍처를 구성한다.
스케일(scale out/up), 회복성(resiliency) 등이 충족된다.
Level 3: 쿠버네티스 패턴(컨테이너)
컨테이너를 통한 애플리케이션 패키징과 실행이 가능해짐에 따라, 대규모의 컨테이너 기반 마이크로서비스 자동화가 요구된다.
쿠버네티스는 컨테이너 오케스트레이션 도구이자 클라우드 네이티브 플랫폼으로서 컨테이너 환경에서의 자동화와 컨테이너를 다루기 위한 다양한 방법을 제시한다. 해당 교재의 주제이기도 하다.
분산 기본 요소
앞서 언급하였듯 MSA는 DDD로부터 출발한다. DDD가 객체를 도메인 별로 나누었다면, MSA는 DDD에서 객체 대신 서비스를 넣었다고 생각할 수 있겠다.
*로컬 기본 요소(in-process): Java 또는 JVM에서 제공하는 빌딩 블록 요소
*분산 기본 요소: 클라우드 네이티브 환경에서의 빌딩 블록 요소 (해당 교재에선 쿠버네티스와 같다고 고려)
개념 | 로컬 기본 요소 | 분산 기본 요소 |
캡슐화 동작 | 클래스 | 컨테이너 이미지 |
인스턴스화 동작 | 객체 | 컨테이너 |
재사용 단위 | .jar 파일 | 컨테이너 이미지 |
컴포지션(Composition) | 클래스 A가 클래스 B를 포함 | 사이드카 패턴 |
상속 | 클래스 A가 클래스 B를 확장 | 'FROM 부모 이미지' 로 만든 컨테이너 이미지 |
배포 단위 | .jar/.war/.ear | 파드 |
빌드 타임 / 런타임 격리 | 모듈, 패키지, 클래스 | 네임스페이스, 파드, 컨테이너 |
초기화 필요 조건 | 생성자 | 초기화 컨테이너 |
초기화 직후 트리고 | Init 메소드 | postStart |
삭제 직전 트리거 | Destroy 메소드 | preStop |
정리(Clenup) 절차 | finalize(), 셧다운 훅 | Defer 컨테이너 (아직 구현 X) |
비동기 & 병렬 실행 | ThreadPoolExcutor, ForkJoinPool | 잡 |
주기적 작업 | Timer, ScheduledExecutorService | 크론잡 |
백그라운드 작업 | 데몬 스레드 | 데몬셋 |
설정 관리 | System.getenv(), Properties | 컨피그맵, 시크릿 |
짚고 넘어가야할 것은
분산 기본 요소와 객체 지향 방식의 로컬 기본 요소는 직접적으로 비교하거나 대체가 일반적으로 불가능하다.
애초에 추상화 레벨이 다르기에 두 개념 간의 기본 요소가 직접적으로 겹친다고는 할 수 없다.
컨테이너와 파드
쿠버네티스를 배우기 시작하는 입장에서 컨테이너와 파드 간의 차이만큼 혼동되는 개념도 없다. 해당 파트는 본격적으로 쿠버네티스 패턴에 들어가기 이전에 쿠버네티스 기본 요소를 정확히 이해하자는 데에 목적을 둔다.
컨테이너는 다음의 특징을 갖는다.
- 쿠버네티스 기반 클라우드 네이티브 애플리케이션의 빌딩 블록 (패키징 및 격리를 제공하는 단위로 기능)
- 컨테이너 이미지: 인스턴스화 이전의 클래스와 유사하다.
- 클래스의 확장 및 수정이 이뤄지듯, 컨테이너 이미지의 버저닝(versioning)을 통한 확장과 수정이 가능하다.
- 하나의 문제를 해결하는 기능 단위이며 독립적인 릴리스 주기를 갖는다.
- 독립된 런타임과 배포를 제공한다.
- 대부분의 경우, 1 마이크로서비스 = 1 컨테이너 이미지 이다.
파드는 다음의 특징을 갖는다.
- 컨테이너 그룹의 수명주기를 관리하기 위한 분산 기본 요소
- 컨테이너 그룹의 스케줄링과 배포, 격리된 런타임에 대한 최소 단위
- 파드 냉부의 모든 컨테이너는 같은 호스트에 스케쥴링
- 파일 시스템, 네트워크 자원, 프로세스 네임스페이스 등을 공유
위의 이유로 인해 파드 내에서는 파일 시스템 / 로컬 호스트 / IPC 등을 통해 컨테이너 간의 상호작용이 가능해진다.
즉, 쿠버네티스 내에서 애플리케이션이 스케쥴링되는 최소 단위는 파드이고, 그 내부에서 실제 실행되는 이미지는 컨테이너라고 이해할 수 있다.
서비스
쿠버네티스를 이루는 가장 주요한 특징 중의 하나는 HA를 위해서 파드가 언제든지 죽고 다시 시작될 수 있다는 점이다. 때문에 기존의 하드웨어에서 이뤄지는 네트워킹과 쿠버네티스에서 이뤄지는 네트워킹은 달라야 한다. 이를 가능하게 해주는 것이 바로 쿠버네티스의 기본 리소스 중 하나인 서비스이다.
주요한 서비스 요소로는 Cluster IP / Node Port / Load Balancer 등이 있으며, 엄밀하게 말해서 앞서 설명한 서비스의 역할을 수행하는 자원은 이 중 Cluster IP이다. ClusterIP는 파드들이 클러스터 내부의 다른 리소스들과 통신할 수 있도록 해주는 가상의 클러스터 전용 IP다. 이 유형의 서비스는 <ClusterIP>로 들어온 클러스터 내부 트래픽을 해당 파드의 <파드IP>:<targetPort>로 넘겨주도록 동작하므로, 오직 클러스터 내부에서만 접근 가능하게 된다.
요약하자면 다음과 같다.
- 언제나 삭제되고 변경될 수 있다는 파드의 특성을 고려한 네트워킹 리소스
- 파드의 주소는 애플리케이션 실행 중에도 변경 가능하다.
- 예를 들어, 클러스터의 노드에 문제가 생기면, 파드는 정책에 따라 다른 노드로 옮겨간다. 스케줄링된 노드가 변경됨에 따라 파드의 IP 주소도 변경된다.
- 대신 서비스에서는 고정 불변한 IP 주소와 포트를 제공한다.
- 서비스는 실제 파드의 IP 주소에 접근하지 않고도, 파드와 연결을 유지할 수 있는 방법을 제공한다.
- 즉, 서비스의 가장 기본적인 역할은 특정 파드의 IP 주소와 포트로의 진입점 역할을 수행하는 것이다.
레이블 과 애노테이션
쿠버네티스를 사용해봤다면 레이블을 통해 클러스터 내에 배포를 해본 경험이 있을 것이다. 레이블 셀렉터와 레이블을 통해 쿠버네티스 리소스들이 어떤 도메인으로 묶이는지를 구분한다고 이해하는 것은 그리 어렵지 않다. 다만, 레이블과 상당히 유사하지만 다른 역할을 수행하는 애노테이션이라는 요소가 있다. 해당 섹션에서는 두 주제 간의 차이를 비교해보고자 한다.
먼저 레이블을 요약하자면 다음과 같다.
- Key: Value 쌍의 Map 형태로 구성됨
- 마이크로서비스로 넘어오면서, 하나의 애플리케이션 레벨로 수행하는 핵심 아티팩트 개념이 사라짐
- 그럼에도 특정 서비스들이 하나의 애플리케이션에 속함을 표현할 경우가 발생 -> 레이블의 활용
- 레이블을 통해 하나의 논리적 단위로 관리 가능, 객체 간의 매칭과 쿼리 시에 활용
- 스케쥴러에서는 요구사항에 맞는 노드에 파드를 배치하기 위해, 레이블을 통해 파드들을 분산시키기도하고, 단일 노드에 위치시키기도 함.
이어서 애노테이션을 요약하자면 다음과 같다.
- Key: Value 쌍의 Map 형태로 구성됨
- 사람보다는 기계를 위한 용도
- 검색 불가능한 메타데이터를 지정하는데 활용
- 주로 다양한 도구나 라이브러리와의 상호작용을 위해 객체에 추가적인 메타데이터를 추가하는 용도
즉 두 요소의 차이를 다음과 같이 이해할 수 있겠다.
- 특정 자원에 동작을 수행하거나 쿼리를 수행 -> Label
- 기계가 인식하는 메타에이터 추가 -> Annotation
네임 스페이스
쿠버네티스 클러스터를 논리적 자원 풀로 나누는 데 활용되는 쿠버네티스 기본 리소스이다. 사실 네임스페이스라는 개념은 쿠버네티스의 고유한 개념은 아니고, 리눅스의 cgroup을 통해 관리되는 네임스페이스로부터 출발한다. 이때 네임스페이스를 통해 격리된 환경들은 멀티 테넌시 환경으로 활용될 수 있다. 해당 클러스터 내의 네임스페이스의 이름은 겹치지 않아야 하며, 이는 노드나 PersistentVolume과 같은 기본 요소도 마찬가지이다.
이때, 주의해야할 점은 네임스페이스가 클러스터 자원에 대해서 논리적인 영역을 제공하긴 하지만, 하나의 자원이 다른 자원에 접근하는 것을 막을 수는 없다는 점이다. 파드의 IP를 무슨 이유에서인지 알고 있다면 이를 통해 내가 가진 프로필과 다른 네임스페이스에 대해서도 접근이 가능하다.
이후 2장에서 소개될 ResourceQuota를 통해 네임스페이스 당 자원 소모량을 제한할 수 있다.
'DevOps' 카테고리의 다른 글
[Kubeflow] pipeline (0) | 2023.04.07 |
---|---|
[Kubeflow] MLOps 개발환경 설정 (0) | 2023.04.07 |
error: parsing file '/var/lib/dpkg/updates/0001' near line 0: newline in field name `#padding' (0) | 2023.03.23 |
Could not get lock /var/lib/dpkg/lock-frontend (0) | 2023.03.23 |
sudo ubuntu-drivers autoinstall 오류 (0) | 2023.03.23 |
문제 상황
문제가 없는 경우 오른쪽 하단의 Github 오토파일럿 아이콘을 클릭하여 enable/disable이 가능하다. 나의 경우에는 클릭해도 아무런 반응이 없었다.
해결
Do Not Distrub Mode가 켜져 있어 해당 아이콘을 눌러도 반응이 없던 것이였다.
CMD + SHIFT + P를 통해 Do Not Distrub Mode를 해제해주었다.
* 혹여 오토파일럿 아이콘이 하단에 보이지 않는 경우 아이콘이 위치한 라인을 우클릭할 경우 아이콘을 추가할 수 있다.
무슨 이유에선지, Crawler로 생성한 Glue Data DB에 스키마 생성이 되지 않았다. 처음에는 데이터가 또 문제가 있어서 그렇겠거니 하고 Glue Studio에서 해결을 해보려고 했으나, 업데이트를 해도 원하는 데이터가 딱 만들어지지 않았다.
대충 이런 데이터가 있었다. (문제가 생길 수 있어 내부 데이터는 실제 데이터 값이 아닌 랜덤으로 생성된 값을 넣었다.)
바로 위의 사진이 변환된 결과이다. 칼럼 이름은 다 어디가버리고, col{0..n}이라고 자기 마음대로 이름이 붙어있다.
Try #1 Crawler의 Classifier 활용
먼저 Classifier를 활용하여 직접 칼럼 이름을 명시해보았다. 그런데 설정을 잘못한 건지 원래 안되는 건지 잘되지 않아 다른 방식으로 넘어갔다.
Crawler에 Classifier를 추가했지만 여전히 col{0..n}으로 표시될 뿐이였다.
Try #2 Glue Studio에서 수작업으로 일일이 이름 지정
가장 시간도 많이 잡아먹고, 바보같은 짓이였는데 이것저것 해보다 안되니 그냥 30여개 정도 되는 칼럼 이름, 내가 손으로 쓰고 끝낸다는 심정으로 시도해봤다.
결론은 실패였는데, 우선 Glue Studio에서 이름을 지정해놓고 RUN을 돌리고 나면, 분명 Data Preview로 봤을 때는 문제 없이 Data Brew Target으로 잘 들어가던 데이터가 확인만 하고 나면 다시 원래 상태로 복귀되는 문제가 발생했다. 처음에는 무언가 문제가 있는 거 같다는 생각에 이렇게도 해보고 저렇게도 해봤는데, 결국 요금만 날라가고 소득이 없었다.
그리고 Data 자체도 Glue에서 정상적으로 받아들이지 못하는 느낌도 있었다.
다음 그림처럼 새로운 Action을 생성하기만 하면 Source Key와 Target 간의 맵핑이 전부 초기화되는 문제가 발생했다. drop을 별도의 Action으로 실행하려고 만든 Action이였는데 문제가 발생해버렸다. 여기서 더 머리가 아팠던 부분은 저 부분을 날리려고 해도, 날릴 수가 없었다.
만약 저 Action을 날리면 이렇게 잘 작성된 Mapping Action이 슝 날라가버린다.
이렇게 말이다. 검토를 꽤 오래했으나 도대체 무슨 문제인지를 모르겠다. Target을 잘못 설정해서 그랬던 거 같기도 하고... Target을 잘못 설정한다고 Source에서 내려온 데이터를 변환한게 무슨 영향을 주겠는가 싶기도 하고... 결국 결론은 잘 모르겠다였다. 이 방법이 아닌가 보다라고 해당 방법은 폐기하기로 마음을 먹게 되었다.
Try #3 CSV 파일 수정
앞서 시간을 쏟았던 게 너무나도 허무하다고 느낄만큼 해결은 간단했다.
문제는 바로 헤더의 맨 처음 값이 비어있었기 때문이였다. Glue의 Crawler는 별도로 명시가 없는 경우, Header를 알아서 인식하고 스키마를 자동으로 작성하여 데이터베이스를 구성한다. 근데 맨 앞에 빈 값이 들어가버리니 이를 인식하지 못했던 거 같다.
두 가지 해결이 가능했다. row 번호를 의미하는 첫 번째 열을 통째로 날리던가, 그냥 칼럼 이름을 넣어주면 된다.
그래서 NO라는 칼럼 이름을 넣어줌으로써 문제를 해결했다. 괜히 어렵게 돌아가려고 하다가 손해가 이만저만이 아니다. 다음에는 이런 문제로 시간을 이렇게는 쓰지 않고 싶다 ㅠㅠ
'Data Engineering' 카테고리의 다른 글
Zookeeper의 znode (0) | 2023.05.18 |
---|---|
Coursera 데이터 엔지니어링 강의 목록 (0) | 2022.12.08 |
Spark (0) | 2022.11.20 |
로그 데이터 수집 (0) | 2022.11.18 |
[AWS Glue] Crawler를 통해 S3에서 Data Catalog로 데이터 추출 (1) | 2022.10.26 |