들어가기에 앞서
이 글은 "Go언어를 활용한 분산 서비스 개발"이라는 책을 보고 정리한 내용입니다
먼저 로그 패키지를 만들기 전에 용어를 머리에 집어넣고 가야 편합니다
레코드(Record) : 로그에 정리할 데이터
저장 파일(Store): 레코드를 저장하는 파일
인덱스 파일(Index): 인덱스를 저장하는 파일
세그먼트(Segnment): 저장 파일과 인덱스 파일을 묶어서 말하는 추상적 개념
로그(Log): 모든 세그먼트를 묶어서 말하는 추상적 개념
1. 스토어(Store) 만들기
로그 패키지를 위한 LetsGo/WriteALogPackage/internal/log/stroe.go
package log
import (
"bufio"
"encoding/binary"
"os"
"sync"
)
var (
enc = binary.BigEndian
//엔디언이란 컴푸터에 바이트가 정렬되는 방식
//BigEndian-> 높은 바이투 부터 저장되는 방식
)
const (
lenWidth = 8 //레코드 길이를 저장하는 바이트 개수
)
type store struct {
*os.File // ->내 컴퓨터의 파일 활용해주게 해준다
mu sync.Mutex //내가 아는 그 뮤텍스 맞음
buf *bufio.Writer //buf에 io 읽고 쓰기 중에 쓰기
size uint64
}
func newStore(f *os.File) (*store, error) { //Store 생성 함수
fi, err := os.Stat(f.Name()) //os.Stat->FileInfo 객체를 가져온다 f.Name()의
if err != nil {
return nil, err
}
size := uint64(fi.Size()) //파일의 사이즈
return &store{ //store 생성
File: f, //객체 저장
size: size, //사이즈 저장
buf: bufio.NewWriter(f), //쓰기 인스턴스 buf 생성
}, nil
}
1. 먼저 Store 구조체를 보자
1. os.File은 os 라이브러리를 활용해서 만든 파일 변수라고 생각하자
2. mu Mutex 다른 자원들과 충돌나지 않게 만든 Mutex이다
3. buf bufio라이브러리를 활용한 buf 변수에 buffer를 쓸 수 있도록 만들었따
4. uint64 -> 부호 없는 정수로 선언했다 약 2에 64승(18,446,744,073,709,551,616)-1 정도 된다..
이렇게 Store 구조체는 정의했고 이제 생성하는 함수를 만들어보자
newStore에서 인자값은 File인 f로 받고 반환은 *store, error를 반환한다
fi, err 에 os.Stat함수를 호출해 파일의 객체를 fi에 저장한다 -> size에 파일 크기 저장 -> &store를 통해 리턴함으로써
Store(레코드를 저장하는 파일)을 만들고 반환해준다 여기서 중요한점은 참조하기때문에 객체를 반환하는 것 + 객체 생성까지 된다
2. 이제 Store(저장 파일)을 만들었으니 메세지를 추가할 수 있는 Append() 메서드를 만들어보자
func (s *store) Append(p []byte) (n uint64, pos uint64, err error) { //추가
s.mu.Lock()
defer s.mu.Unlock()
pos = s.size //Store의 사이즈
if err := binary.Write(s.buf, enc, uint64(len(p))); err != nil { //쓸 수 있는지
return 0, 0, err
}
w, err := s.buf.Write(p) //받은 메세지를 Store.buf에 쓴다(w에 쓴 바이트 크기를 저장)
if err != nil {
return 0, 0, err
}
w += lenWidth //사이즈 -> 쓴 바이트 + 레코드 저장할 바이트
s.size += uint64(w)
return uint64(w), pos, nil //쓴 바이트 크기, 시작한 바이트 위치 return
}
Append메서드는 직접 byte slice 타입인 p를 받아서 저장 파일에 쓴다
Store(저장 파일)의 크기를 pos라는 변수에 저장하고 binary.Write 메소드는 공식 문서를 봐도 이해하기 어려웠습니다
제가 유추하기로는 아마 s.buf에 쓸 수 있는지 확인하는 용도인 거 같습니다
s.buf.Write(p)는 p(byte Slice 타입)을 받아 s.buf에 내용을 쓰고 w에 쓴 바이트 크기를 반환합니다.
-> 굳이 s.size에 더하지 않고 버퍼를 거쳐 저장하는 이유는 System call 횟수를 줄여 성능을 개선하기 위해서입니다!! 대단하네요
w에 lenWidth를 더해 (바이트 크기 + 레코드에 저장할 바이트 개수)
이제 s.size에 p만큼의 바이트 크기 + 레코드에 저장할 바이트 개수를 저장해줍시다
그리고 실제로 쓴 바이트 수(w), 어느 위치에 썼는지(pos)를 반환해주면 끝입니다
3. Read메서드
func (s *store) Read(pos uint64) ([]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
if err := s.buf.Flush(); err != nil { //레코드가 버퍼에 남아 있다면 Flush로 디스크에 쓴다
return nil, err
}
size := make([]byte, lenWidth) //size슬라이스를 동적할당느낌
if _, err := s.File.ReadAt(size, int64(pos)); err != nil { //다음 읽을 레코드바이트 크기
return nil, err
}
b := make([]byte, enc.Uint64(size)) //높은순으로 정렬되게 동적할당
if _, err := s.File.ReadAt(b, int64(pos+lenWidth)); err != nil {
return nil, err
}
return b, nil
}
먼저 레코드가 버퍼에 남아 있다면 Flush 함수를 이용해 디스크에 쓰고
b에는 s.File의 데이터를 읽어 가져옵니다
4. ReadAt 메소드
func (s *store) ReadAt(p []byte, off int64) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
if err := s.buf.Flush(); err != nil {
return 0, err
}
return s.File.ReadAt(p, off)
}
이후 "s.File.ReadAt(p, off)" 메서드를 호출하여 실제 파일에서 데이터를 읽어옵니다.
5. Close 메소드
func (s *store) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
if err := s.buf.Flush(); err != nil {
return err
}
return s.File.Close()
}
Close() 메서드는 파일을 닫기 전 버퍼에 데이터를 쓰는 기능을 담당합니다
Store 로직을 구현했으니 바로 test해보겠습니다
그 전에 testing 패키지를 간단히 설명하겠습니다
"go test"는 현재 폴더에 있는 *_.test.go 파일들을 일괄적으로 테스트 코드로 인식해 실행합니다
테스트 메서드는 TestXx..와 같이 작성하고 Xx에서 첫 글자는 항상 대문자여야 합니다 그리고 메서드에 인자는 testing.T 포인터 하나 입력으로 받으며 출력은 따로 없습니다
TestStoreAppendRead
var (
write = []byte("hello world") //보낼 메세지
width = uint64(len(write)) + lenWidth
)
func TestStoreAppendRead(t *testing.T) {
f, err := os.CreateTemp("", "store_append_read_test") //f는 store...라는 파일
require.NoError(t, err)
defer os.Remove(f.Name())
s, err := newStore(f) //해당 파일을 열고 새로운 Store 생성
require.NoError(t, err)
testAppend(t, s) //데이터 추가
testRead(t, s) //데이터 읽기
testReadAt(t, s) //데이터 검증
s, err = newStore(f)
require.NoError(t, err)
testRead(t, s) //이전 추가한 데이터가 읽히는지 확인
}
store_apppend_read_test라는 파일을 만듭니다 -> newStore() 메서드로 해당 파일을 열고 새로운 Store를 생성하고
-> 데이터를 추가 -> 데이터 읽기 -> 데이터 검증 -> 해당 파일의 새로운 Store 생성 -> 이전에 추가한 데이터가 읽히는지 확인
나머지 testcode들입니다
func testAppend(t *testing.T, s *store) {
t.Helper()
for i := uint64(1); i < 4; i++ {
n, pos, err := s.Append(write) //write 데이터를 추가
require.NoError(t, err) //에러 확인
require.Equal(t, pos+n, width*i) //각 데이터가 예상 위치에 추가됬는지 확인
}
}
func testRead(t *testing.T, s *store) {
t.Helper()
var pos uint64
for i := uint64(1); i < 4; i++ {
read, err := s.Read(pos)
require.NoError(t, err)
require.Equal(t, write, read)
pos += width
}
}
func testReadAt(t *testing.T, s *store) {
t.Helper()
for i, off := uint64(1), int64(0); i < 4; i++ {
b := make([]byte, lenWidth)
n, err := s.ReadAt(b, off)
require.NoError(t, err)
require.Equal(t, lenWidth, n)
off += int64(n)
size := enc.Uint64(b)
b = make([]byte, size)
n, err = s.ReadAt(b, off)
require.NoError(t, err)
require.Equal(t, write, b)
require.Equal(t, int(size), n)
off += int64(n)
}
}
처음 작성한 로직에서 함수들은 test하는 메서드들입니다
실행해보면 적상적으로 돌아가는 것을 알 수 있습니다
이제 다음 글은 Index를 이용해 로그들을 확인해보겠습니다
전체 코드
https://github.com/sleeg00/go/tree/main/LetsGo
* 틀린 점이 있거나 질문이 있다면 댓글 꼭 부탁드립니다 *
'Go' 카테고리의 다른 글
[Go] Go Clean Architecture란 무엇인가? (0) | 2023.05.01 |
---|---|
[Go] Go의 웹 프레임워크 Gin이란? (0) | 2023.04.29 |
[Go] golang slice안에 특징 (0) | 2023.02.16 |
[Go] Go루틴이란 무엇인가? (0) | 2023.02.16 |
[Go] struct를 생성하고 error코드 작성하기 (0) | 2023.01.09 |