1. 백준 치킨배달[15686]

도시가 주어지고, 도시에는 집과 치킨가게 가 존재한다. 총 m 개의 치킨가게 가 존재할 때 도시에 주어지는 집들로부터 거리가 가장 작은 최솟값의 거리를 구하는 문제이다. 

치킨가게 와 집의 거리계산 은 주어지는 좌표 의 차에서 구한 거리이다.

 

해당 문제 는 주어지는 배열을 사용하지 않고, 치킨집과 집들을 가지고 새로운 거리 배열을 만들어서 해결해 주었다. 

다시 말해 행, 열 에는 치킨집과 집들을 배치하고, 해당 거리들을 배열에 담아 해결했다.

 

func boj15686() {
	answer := 1 << 31
	var distance [][]int
	abs := func(a int) int {
		if a < 0 {
			return -a
		}
		return a
	}
	min := func(a, b int) int {
		if a < b {
			return a
		}
		return b
	}

	minValue := func(include []int, distance [][]int) int {
		result := 0
		for i := 0; i < len(distance); i++ {
			step := 1 << 31
			for j := 0; j < len(distance[i]); j++ {
				for k := 0; k < len(include); k++ {
					if j == include[k] {
						step = min(step, distance[i][j])
					}
				}
			}
			result += step
		}
		return result
	}

	helper := func(num, depth, limit, flag, start int, out []int) {}
	helper = func(num, depth, limit, flag, start int, out []int) {
		if depth == limit {
			answer = min(answer, minValue(out, distance))
			return
		} else {
			for i := start; i < num; i++ {
				if flag&(1<<i) == 0 {
					helper(num, depth+1, limit, flag|(1<<i), i+1, append(out, i))
				}
			}
		}
	}
	reader := bufio.NewReader(os.Stdin)

	var (
		a, b int
	)
	fmt.Fscanln(reader, &a, &b)
	house := make([][]int, 0)
	chicken := make([][]int, 0)

	for i := 0; i < a; i++ {
		for j := 0; j < a; j++ {
			var tmp int
			fmt.Fscan(reader, &tmp)
			if tmp == 1 {
				house = append(house, []int{i, j})
			} else if tmp == 2 {
				chicken = append(chicken, []int{i, j})
			}
		}
	}

	distance = make([][]int, len(house))
	for i := 0; i < len(house); i++ {
		distance[i] = make([]int, len(chicken))
		for j := 0; j < len(chicken); j++ {
			distance[i][j] = abs(house[i][0]-chicken[j][0]) + abs(house[i][1]-chicken[j][1])
		}
	}

	/**
치킨집의 개수가 최대 13개이므로, 치킨집의 개수에서 M개를 뽑는 조합을 구하면 된다. (13C6) = 1716 * 집이 최대 50개이므로, 1716 * 50 = 85800
	*/
	helper(len(chicken), 0, b, 0, 0, []int{})

	fmt.Println(answer)
}

고에서 제공하는 min, abs는 타입변환이 필요해서 추가적으로 함수를 작성해 주었다.

치킨집, 일반집 은 각각 1,2의 값으로 주어지기 때문에 입력값을 받아옴과 동시에 해당 좌표들을 배열로 만들어주고, 

거리를 구하기 위해서 위에서 작성된 자표를 이용해 모든 집과 모든 치킨집의 대한 거리에 대해 배열을 작성해 주었다.

 

2. 백준 테트로미노 [14500]

왼쪽에 보이는 모양을 맵에다가 놓았을 때 최댓값을 구해야 한다. 단 주어지는 저 도형들은 회전, 대칭 등이 가능하다. 

 

코드가 엄청 길다. 해당되는 상황에 대해 모두 분기를 나눠주다 보니 아무래도 그게 더 심했던 거 같다.

 

코드보다 그림을 더 많이 그렸던 문제...

 

func solution() {
	reader := bufio.NewReader(os.Stdin)
	getMax := func(a, b int) int {
		if a > b {
			return a
		}
		return b
	}
	stick := func(arr [][]int, row, col int) (rs int) {
		//right 3steps more
		if col+3 < len(arr[0]) {
			rs = getMax(rs, arr[row][col]+arr[row][col+1]+arr[row][col+2]+arr[row][col+3])
		}
		//down 3steps more
		if row+3 < len(arr) {
			rs = getMax(rs, arr[row][col]+arr[row+1][col]+arr[row+2][col]+arr[row+3][col])
		}
		return rs
	}
	square := func(arr [][]int, row, col int) (rs int) {
		// right 1step down 1step
		if row+1 < len(arr) && col+1 < len(arr[0]) {
			rs = getMax(rs, arr[row][col]+arr[row][col+1]+arr[row+1][col]+arr[row+1][col+1])
		}
		return rs
	}
	nieun := func(arr [][]int, row, col int) (rs int) {
		// right 2steps down 1step
		if row+1 < len(arr) && col+2 < len(arr[0]) {
			rs = getMax(rs, arr[row][col]+arr[row+1][col]+arr[row+1][col+1]+arr[row+1][col+2])
		}
		// left 2steps down 1step
		if row+1 < len(arr) && col-2 >= 0 {
			rs = getMax(rs, arr[row][col]+arr[row+1][col]+arr[row+1][col-1]+arr[row+1][col-2])
		}
		// top 1step right 2steps
		if row-1 >= 0 && col+2 < len(arr[0]) {
			rs = getMax(rs, arr[row][col]+arr[row-1][col]+arr[row-1][col+1]+arr[row-1][col+2])
		}
		// top 1step left 2steps
		if row-1 >= 0 && col-2 >= 0 {
			rs = getMax(rs, arr[row][col]+arr[row-1][col]+arr[row-1][col-1]+arr[row-1][col-2])
		}

		//right 1step down 2steps
		if row+2 < len(arr) && col+1 < len(arr[0]) {
			rs = getMax(rs, arr[row][col]+arr[row][col+1]+arr[row+1][col+1]+arr[row+2][col+1])
		}
		//right 1step top 2steps
		if row-2 >= 0 && col+1 < len(arr[0]) {
			rs = getMax(rs, arr[row][col]+arr[row][col+1]+arr[row-1][col+1]+arr[row-2][col+1])
		}
		// left 1step down 2steps
		if row+2 < len(arr) && col-1 >= 0 {
			rs = getMax(rs, arr[row][col]+arr[row][col-1]+arr[row+1][col-1]+arr[row+2][col-1])
		}
		// left 1step top 2steps
		if row-2 >= 0 && col-1 >= 0 {
			rs = getMax(rs, arr[row][col]+arr[row][col-1]+arr[row-1][col-1]+arr[row-2][col-1])
		}
		return rs
	}
	triangle := func(arr [][]int, row, col int) (rs int) {
		// right 2steps up 1step
		if row-1 >= 0 && col+2 < len(arr[0]) {
			rs = getMax(rs, arr[row][col]+arr[row][col+1]+arr[row][col+2]+arr[row-1][col+1])
		}
		//right 2steps down 1step
		if row+1 < len(arr) && col+2 < len(arr[0]) {
			rs = getMax(rs, arr[row][col]+arr[row][col+1]+arr[row][col+2]+arr[row+1][col+1])
		}

		// down 2steps right 1step
		if row+2 < len(arr) && col+1 < len(arr[0]) {
			rs = getMax(rs, arr[row][col]+arr[row+1][col]+arr[row+2][col]+arr[row+1][col+1])
		}
		// down 2steps left 1step
		if row+2 < len(arr) && col-1 >= 0 {
			rs = getMax(rs, arr[row][col]+arr[row+1][col]+arr[row+2][col]+arr[row+1][col-1])
		}
		return rs
	}
	etc := func(arr [][]int, row, col int) (rs int) {
		// right 2steps up 1step
		if row-1 >= 0 && col+2 < len(arr[0]) {
			rs = getMax(rs, arr[row][col]+arr[row][col+1]+arr[row-1][col+1]+arr[row-1][col+2])
		}
		//right 2steps down 1step
		if row+1 < len(arr) && col+2 < len(arr[0]) {
			rs = getMax(rs, arr[row][col]+arr[row][col+1]+arr[row+1][col+1]+arr[row+1][col+2])
		}

		//down 2steps right 1step
		if row+2 < len(arr) && col+1 < len(arr[0]) {
			rs = getMax(rs, arr[row][col]+arr[row+1][col]+arr[row+1][col+1]+arr[row+2][col+1])
		}
		//down2steps left 1step
		if row+2 < len(arr) && col-1 >= 0 {
			rs = getMax(rs, arr[row][col]+arr[row+1][col]+arr[row+1][col-1]+arr[row+2][col-1])
		}
		return rs
	}

	check := func(arr [][]int, row, col int) int {
		result := getMax(square(arr, row, col), stick(arr, row, col))
		result2 := getMax(nieun(arr, row, col), triangle(arr, row, col))
		return getMax(result, getMax(result2, etc(arr, row, col)))
	}

	var answer int
	helper := func(arr [][]int, size int) {}
	helper = func(arr [][]int, size int) {
		if size == len(arr[0])*len(arr) {
			return
		}
		row, col := size/len(arr[0]), size%len(arr[0])
		answer = getMax(answer, check(arr, row, col))
		helper(arr, size+1)
	}

	var n, m int

	fmt.Fscanln(reader, &n, &m)

	arr := make([][]int, n)
	for i := range arr {
		arr[i] = make([]int, m)
		for j := range arr[i] {
			fmt.Fscan(reader, &arr[i][j])
		}
	}

	helper(arr, 0)
	fmt.Println(answer)
}

 

 

단순 코드가 길어서 그렇지 해당되는 모든 케이스에 대해서 잘 정리만 해준다면 일반적인 dfs처럼 좌표의 1번부터 수행하면 된다.

개인적으로 이렇게 좌표탐색을 할 때는 row, col을 넘겨주는거보다 하나의 숫자를 넘겨 row,col 을 계산하는 것이 훨씬 깔끔하다. 예전 스도쿠 문제 풀면서 다른 사람 풀이 중에 이런 부분이 있어 바로 차용해서 이제는 저렇게 숫자로 넘기는 게 더 편하다.

 

이번주는 생각보다 문제를 많이 풀지 못했다. 아침에 가면 문의 답변 해야 할게 생각보다 있어 오전시간에는 거의 풀지 못했다.

 

다만 지난주보다 마음에 들었던 점은 해당 두 문제에 대해서 바로 한 번에 통과해서 기분이 무척 좋았다.

 

'PS > Boj' 카테고리의 다른 글

백준-완탐 연습(Brute-Force)  (0) 2023.08.14
백준 1251 단어나누기[Java]  (0) 2022.06.26
백준 1120 문자열[Java]  (0) 2022.06.26
백준 10448 유레카[Java]  (0) 2022.06.26

프로젝트를 하면 할수록, 내가 명확하게 무엇을 아는가? 에 대해서 고민하게 되었고. Ultimate Go라는 글을 읽어보고자 한다. 

사실 Go Slack에서 물어보니 이거랑, Go in Action을 추천해 주더라 먼저 Ultimate Go를 읽어보자. 

문법

- 지금 와서 생각해 보면 바이트, 비트의 개념이 생각보다 많이 없지 않았나 싶다. 

1바이트 => 8비트이다. 그렇다면 +- 부호비트를 제외하고 7자리 최댓값은 2의 7승 127까지 표현된다.  특히나 고에서는 이러한 표현에 있어 그냥 넘어가는 게 아닌 기민하게 받아들여야 한다.

생성되는 모든 변수는 초기화되어야 한다. 어떤 값으로 초기화할지 명시하지 않는다면, 제로값으로 초기화된다. 할당된 메모리의 모든 비트는 0으로 리셋된다.

- 사실 저 부분 4번 읽어봤다. 이해한 바에 따르면 언어의 변수를 생성하고 값을 명시하지 않는다. 예를 들어 

var a int64라고 가정한다면 이는 8바이트짜리 메모리 공간을 할당한다.

즉 메모리의 시작주소와 크기가 정해져 있지만 값을 할당하지 않아 8바이트 의 모든 비트는 00000000..... 0000으로 표기된다는 의미이다.

 

문자열은 uint8 타입의 연속이다. 

- 문자열 가지고 for 문 돌려보면 rune 타입으로 반한 된다, 여기서 rune 은 int32의 alias 별칭타입이다 그냥 숫자 덩어리이다. 

그러면 의문이 생긴다. uint8과 rune 은 엄연히 다른 타입 즉 메모리 사이즈 가 다르다. 왜 다를까?

 

1. 문자열 : uint8 타입의 연속이고 이는 UTF8 인코딩 된 문자를 나타낸다.

2. rune : unit32의 별칭이고, 주로 Unicode 코드포인트를 표현하는 데 사용된다.

 

다시 말하자면 utf8 인코딩 은 다양한 길이의 바이트 시퀀스를 이용해 유니코드 문자를 나타낸다. 

어떤 문자는 1바이트, 어떤 문자는 2,3,4 바이트로 나타내야 할 수도 있다. 그렇기 때문에 for 문을 이용해서 range 처리를 하게 되면

uint8로 표현을 하게 되면 올바르게 utf-8 을표현할 수가 없다. 따라서 rune을 이용해 반환하게 된다. 

 

유니코드 : 세계의 모든 문자를 컴퓨터에서 일관되게 표현하고 다룰 수 있도록 설계된 산업표준으로 코드포인트가 존재한다.
예를 들어 ) ㅇ : U+3147 , 안 
UTF-8 : 유니코드를 바이트 시퀀스로 인코딩하는 방식이다.
func Test_String_Rune(t *testing.T){
	a := "안녕"
}

위와 같이 a라는 값이 할당되면 우리의 명석한 고 컴파일러 님은 

1. 안과 녕 에 해당하는 유니코드 포인트로 해석을 하고 

2. 해석된 값을 UTF-8로 인코딩을 하고 (한글은 3바이트) 총 6바이트 길이의 배열이 필요하고 이에 메모리 할당을 하고

3. 문자열 a는 해당된 문자열 바이트 의 주소 시작값을 가지고, 6이라는 사이즈의 길이를 가진다.

이렇게 되면 len(a) 문자열의 길이를 추출하게 될 때 이는 6이라는 숫자를 반환한다.

실제 "안녕"에 대해 추출하고 싶을 때 rune을 이용하게 된다.

다시 말해 

a의 길이를 뽑으면 6이 될 것이고, rune 으로 변환해서 조회한다면 2라는 크기가 나온다.

for 루프를 이용해서 a를 조회하면 총 6번의 값을 조회해서 나타내주고, for... range를 이용해서 조회하면 유니코드 기준으로 조회되어 2번의 값을 조회해서 나타내준다.

for loop의 바이트 시퀀스 조회하는 것의 복잡성을 숨기고자 for... range는 문자열을 더 자연스럽게 순회가 가능하다.

func Test_Variable_String_Rune(t *testing.T) {
	a := "안녕"

	for i := range a {
		fmt.Printf("%v ", a[i])
	}
	fmt.Println("------------------")
	for i := 0; i < len(a); i++ {
		fmt.Printf("%v ", a[i])
	}
	fmt.Println()
}

그렇다면 for loop 의 바이트를 문자열로 조회하고자 한다면? 

func Test_Variable_String_Rune(t *testing.T) {
	a := "안녕"

	for i := 0; i < len(a); {
		r, size := utf8.DecodeRuneInString(a[i:])
		fmt.Printf("%d\t%c\n", i, r)
		i += size
	}
}

이렇게 조회를 하게 되면 "안", "녕"으로 조회가 가능하다. 해당 함수 utf8.DecodeRuneInString(s string)를 살펴보면

내부적으로 단순하게 주어진 s에 대해서 0~3까지 총 4개의 자리에 대해서 탐색을 한다. 즉 4바이트 탐색을 실시하고, 

그거에 맞춰 rune과 크기 값을 반환한다. 각 바이트 별 검사하는 로직은 비트연산과 미리 정해놓은 상수값들을 이용해 비교하고 반환을 해주는데 생각보다 코드가 어지럽지만 해당 함수가 어떻게 검사하고 반환하는지에 대해 살펴보았다.

 

구조체 선언과 구조체 패딩

- 구조체에 할당된 메모리에는 메모리 패딩이라는 것이 존재한다.

func Test_Memeory_Address(t *testing.T) {
	type ex struct {
		counter int64
		pi      float32
		flag    bool
	}

	type ex2 struct {
		flag    bool
		counter int64
		pi      float32
	}

	var e ex
	var e2 ex2

	fmt.Println(unsafe.Sizeof(e), unsafe.Sizeof(e2))
}

 

 

해당 테스트의 결괏값으로 e는 16 e2는 24의 메모리 사이즈를 가져간다 왜 동일한 구조체에 서로 다른 메모리 사이즈일까? 고 언어 에는 메모리패딩을 주어 cpu 가 각 경계별로 손쉽게 읽을 수 있도록 해주고 있다. 

 

ex1의 경우 8바이트(counter) + 4바이트(pi) + 패딩 3바이트 + 1바이트(flag) => 이렇게 총 16바이트가 된다.

ex2의 경우 1바이트(flag) + 7바이트(패딩) + 8바이트(counter) + 4바이트(pi)+4바이트(구조체패딩) => 이렇게 24바이트가 된다.

 

구조체 패딩은 구조체 내에 가장 큰 바이트 기준의 배수로 구조체가 정렬되어야 하기 때문에 4바이트의 추가 패딩이 붙는다.

구조체의 패딩을 제외한다면 실제 구조체의 패딩에 각각 3,7 바이트의 차이가 존재한다.

 

구조체를 뭐 어마무시하게 많은 필드를 넣어서 생성하지는 않겠지만 이러한 규칙이 있어 메모리의 효율적인 사용을 위한다면 가장 큰 메모리를 앞단에 위치시켜야 한다는 사시을 알아야 한다. 

 

포인터 항상 값을 전달한다

- 함수는 함수자체적으로  스택프레임을 가진다. 즉 함수 내에서 사용되고자 하는 값들에 대해 스택프레임에 위치해 해당 함수를 호출하면 모두 제로값으로 초기화되고 함수가 종료되면 스택 은알아서 정리가 된다. 이에 따라 다른 함수에서 해당 값에 접근을 할 수가 없다.

이에 값의 공유 고 루틴(고 루틴 또한 일반적으로 2K 스택메모리를 가지게 된다.) 이러한 고 루틴의 스택들 간에 값을 공유하기 위해서는?

포인터라는 값을 공유해야 한다.

func stayOnStack() user {
	u := user{
		name:  "Ho",
		email: "email",
	}
	return u
}

func escapeToHeap() *user {
	u := user{
		name:  "Ho",
		email: "email",
	}
	return &u
}

해당 함수들을 보면 stayOnStack의 경우 해당 값을 반환함과 동시에 함수 내의 u는 스택에 적재되어 있다가 한 번에 같이 사라지게 된다.

반면 esacpeToHeap을 보면 u의 주소값은 함수 밖을 나와 메모리의 값이 반환되어 스택이 아닌 힙에 적재된다.

이를 go에서는 이스케이프 분석이라고 하며, 변수의 생명주기를 컴파일러가 스택에 넣을지 힙에넣을지 여부를 판단하여 할당하는 것을 의미한다.

func Test_Pointer_Address(t *testing.T) {
	fmt.Println(stayOnStack())
	fmt.Println(escapeToHeap())
	// go test -gcflags '-m -l' advance/variable_test.go
}

go test -gcflags=' -m -l' variable_test.go

위에 테스트를 제시된 코드로 실행하게 되면 아래와 같은 결과를 받을 수 있다.

./variable_test.go:61:25: stayOnStack() escapes to heap
./variable_test.go:62:13:... argument does not escape

음? stayOnStack() 은 heap으로 빠지면 안 된다 왜 빠진 거고, 포인터를 반환하는 escapeToHeap 은 왜 이스케이프 되지 않았는가? 
fmt.Println()의 함수 인자값으로 넘길 때 해당 인자를 힙으로 이스케이프 되었고, escapeToHeap의 경우는 이미 힙으로 이스케이프 되어있기 때문에 할 필요가 없어 위와 같은 메시지를 받게 되는 것이다.

 

생각보다 모르는 부분이 많았고, 정말 언어 개발자들은 천재이지 아닌가 싶다.

'Go > Go Basic' 카테고리의 다른 글

Ultimate-Go-03  (2) 2023.09.07
Ulitmate-Go-02  (0) 2023.09.05
Go Interface, embedded  (0) 2023.02.28
Effective Go 04  (0) 2023.02.12
Effective Go 03  (0) 2023.02.10

7월 개발은 지난달에 개발하고 완료된 상태가 배포도 안 된 상태에서 다시 새로운 기능개발 인 투표/설문 개발 을하게되었다.

새로운 신규개발과 더불어 지난달 개발된 항목에 대해 지속적인 API 지원요청 이 있어 로그확인등 하는데 생각보다 작업하는 일정이 늘어졌다.

 

1. https 오류 지원 

현재 개발의 부모페이지 즉 프런트 부모페이지는 외부협력사에서 담당하고 있다. 해당 개발사에서 는 특정 테스트 서버환경을 제공해주지 않고 단순 우리의 API를 호출하면서 "안된다"라는 메일을 받으면 정말 당황스럽다. 

지난 6월 리버스 프락시 를 적용해서 openssl -x509 를 사설 인증서를 만들어 프록시 서버를 오픈하여 포트를 공유하였다. 이에 대한 피드백은 지지난주 금요일 퇴근 5시 전에 단순 안된다 라는 메일 이 왔고, 이를 해결하기 위해 let's encrypt를 활용해 3개월짜리 무료 ca 인증을 받을 수 있다. 

 

[certbot 인증과정]

lets encrypt 사이트를 접속하게 되면, certbot acme 클라이언트를 이용해 발급받는 방법을 추천하는데 이 방법을 이용해서 인증서 발급을 받았다. 

acme 란 웹보안인증서를 자동으로 발급, 갱신을 위한 프로토콜이다. certbot 웹 페이지를 실제 접근하게 되면 정말 설명이 잘되어 있다.

설명서를 읽기 위해 우선 리눅스의 버전을 알아야 한다. 

 

확실히 리눅스 쪽에서 작업이 어색하다 보니 자주 사용하는 명령어인 scp mv cp ltr grep 등의 명령어만 사용할 줄 알지 다른 거는 정말 모른다. 

리눅스 버전확인 : cat /etc/os-release

centos - 7을 활용하고 있다. 해당 버전을 certbot에 기입을 하게 되면 설치하는 방법에 대해 설명해 준다.

1번 스텝 : ssh를 이용해 서버에 접속한다 

2번 스텝 : snapd를 설치한다.(https://snapcraft.io/docs/installing-snap-on-centos)

리눅스 에는 os에 맞는 버전관리 툴? 이 존재한다 우분투는 apt centos는 yum 등이 있는데 이를 통합해서 관리하는 snapd라는 툴이 새로 나온 것이다. 당연히 우리 서버에는 존재하지 않기에 설치해주어야 했다.

3번 스텝: certbot을 설치한다.

sudo snap install --classic certbot

4번 스탭 : certbot 명령어를 활성화시켜준다.

sudo ln -s /snap/bin/certbot /usr/bin/certbot

ln 링크의 약자로 파일 또는 디렉터리 연결을 할 때 사용하는 것이다.  -s와 조합하여 사용하는 것으로 

/snap/bin/cerbot을 /usr/bin/certbot으로 심벌링 링크 즉 원본파일을 가리키는 링크를 생성하는 것이다. 

이에 따라 /usr/bin 은 환경변수에 포함되어 있기에 리눅스 내에서 certbot 커맨드를 사용할 수 있는 것이다.

 

5번 스탭: certbot을 이용해 어떻게 인증받을지 정하는 것인데 2가지 선택지가 있다.
실제로 해당 웹페이지에 가면 더 다양한 방법을 알려준다. (https://eff-certbot.readthedocs.io/en/stable/using.html)

- 웹서버가 현재 실행 중이 아니라면? 

sudo certbot certonly --standalone

- 웹서버가 계속 실행되는 것이 유지되어야 한다면?

sudo certbot certonly --webroot

 

 

1번의 경우는 80번 포트를 바인딩해서 도메인 인증 저을 한다고 한다. 따라서 웹서버가 진행 중이라면 모두 정지해주어야 한다.

실제로 회사에서는 80번 포트를 운영하는 것 이 nginx 가 있어서 해당 경우는 사용할 수 없었다.

 

2번의 경우는 계속 인증에 실패했다.

2번의 인증 동작방식 중 하나인 /. well-known/acme-challenge 패스에 접근이 되어야 하는데 그것이 불가능해서 발생되는 오류였다.

팀장님께서는 휴가 중이시고 이 리버스 프락시 서버를 만들게 된 계기도 nginx 설정을 만지지 말라고 하셔서 생긴 것이기 때문에 nginx에 추가적인 설정으로 해당 주소를 바인딩하기로 결정했으나 잉? 이게 무슨 일인가 nginx는 서버에서 돌아가고 있지 않은 게 아닌가... 

http {
        include mime.types;
        server {
                listen 80;
                listen [::]:80;
                location ^~/.well-known/acme-challenge/{
                        default_type "text/plain";
                        root /var/www/letsencrypt;
                }
        }
}

바로 설정 추가해서 돌려주었다. 잉 웬걸 서버 실행에 자꾸 실패한다. 확인해 보니. apache httpd 서버가 돌아가고 있는 게 아닌가?

httpd라는 웹서버가 있다는 것도 이때 처음 알게 되었는데 검색을 해보니 nginx와 같은 웹서버 역할을 해준다. 그렇다면 config를 해줄 수 있는 무언가가 있다고 생각해 찾아보니 

/etc/httpd/conf/httpd.conf라는 파일이 존재한다. 해당 폴더에 웹루트에 대해 적혀있고 해당 루트를 기준으로 인증에 필요한 폴더 패스 f를 생성해 주었다.

띠용 계속 실패한다. 이사님에게 여쭤보니 우리의 도메인은 route53을 이용하고 있다고 하신다. route53 의설정을 찾아보니 (https://certbot-dns-route53.readthedocs.io/en/stable/)

aws의 키파일들이 /. aws/config 에 존재해야 한다. 이사님도 계시지 않고, 팀장님도 휴가 중이셔서 다른 방법을 찾아봐야 할 것 같다. 

 

httpd 서버를 통해 특정 포트 혹은 실행 중인 포트와 연동되는 것이 없어 httpd를 일시중지 시키고 1번의 경우를 이용해 certbot을 인증을 받게 되었다. 만약 중지되어서 lsof -i :80에 아무것도 나오면 안 되는 것이니 이중체크 하기 바란다.

 

인증을 받게 되면 /etc/letsencrypt/live/도메인으로 cert.pem, chain.pem, fullchai.pem, privkey.pem READM를 파일들을 준다. 

리버스 프락시에 제공된 cert.pem과 privkey.pem을 이용해 서버를 재기동하고 확인하니 정상적으로 동작하고 있다. 

이렇게 완료되는 줄 알았으나. 다시 받게 된 "안된다 메일"과 함께 온 에러메시지

curl failed to verify the legitimacy of the server and therefore could not establish a secure connection to it.
To learn more about this situation and how to fix it, please visit the web page mentioned above.

너무 답답해서 전화를 해서 확인을 해보니 다른 컴퓨터에서는 정상접근이 가능하나 해당 작업하는 서버에서는 curl 요청 시 위와 같은 에러가 발생한다는 거였다...

문제의 원인은 저 cert.pem 이 문제였다. 해당. pem 은 자체적인 서버인증 만을 가지고 있는 것이다. 다시 말해 웹스에서는 정상접근이 가능하다 그러나 curl을 이용해서 서버의 인증서를 이용해 요청을 하게 될 때 몇몇 서버에서는 해당. pem을 인증할 수 없는 문제가 발생된다. 

README에 작성되어 있는데 한 번이라도 봤더라면 fullchain.pem을 적용했을 텐데 참 아쉽다... 

이에 따라 fullchain을 적용해서 아래 적용되었는지 확인을 하게 되면 아래와 같이 chain 이 형성된다.

openssl s_client -connect "리버스 프록시 주소"


Certificate chain
 0 s:/CN=im.plea.kr
   i:/C=US/O=Let's Encrypt/CN=R3
 1 s:/C=US/O=Let's Encrypt/CN=R3
   i:/C=US/O=Internet Security Research Group/CN=ISRG Root X1
 2 s:/C=US/O=Internet Security Research Group/CN=ISRG Root X1
   i:/O=Digital Signature Trust Co./CN=DST Root CA X3

 

이후 더 이상의 메일은 없었다고 한다. 

 

2. Log 파일 읽기

 

특정 API 호출 시 데이터가 없다는 메일을 수신했다. 제발 이런 메일을 보내면 언제 주로 테스트를 했고 어떻게 했는지에 대해 첨부 좀 해주었으면 좋겠다. 모든 걸 다 확인해야 하기 때문에 생각보다 시간이 오래 걸린다. 

scp로 메일 받기 전날짜의 로그를 로컬로 가져와 sublime Text를 이용해 검색했다.

특정 IP 기준으로 확인을 해보니 진짜 데이터가 안 내려간다. 로직상 에서 데이터가 안 내려가는 경우는 jwt 토큰 인증이 안된경우만 안내려가는 것인데 무엇이 문제인가 로그를 위아래 확인해 보니 세상에 jwt 토큰을 헤더에 잘못 넣어주고 있었다.

우리는 A라고 API 명세서 상에 적어주었으나, B라고 작성해 요청을 하는 것이 아닌가? 이거는 다시 말해서 저 외부협력사는 6월 중순부터 토큰을 넣어서 api 요청을 해본 적이 없다는 소리 아니겠는가?

이런 협력사와 일을 하는 게 너무 스트레스받는다.

 

3.  투표/설문조사 기능 개발

 

투표/설문조사라는 새로운 기능개발 건이 들어왔다. 기존에 없는 신규 기능이기에 처음부터 작성해야 해서 정말 재밌었다. 

ERD 설계, API 명세서 작성 등 4일 동안 팀장님한테 와장창 깨졌다. 

ERD와 API 명세서를 작성하는데 왜 동시성에 대한 문제와 쿼리에 대해 고민하는지 사적지식을 그만 탐구하라고 하셨다.

 

저런 거를 고민 안 하고 어떻게 ERD, API 명세서를 작성하는지 이해가 안 간다. 동시성을 고민해야 했던 이유는 라이브 방송에서 진행되는 투표/설문조사 의 경우 어떻게 통계 테이블에 데이터를 밀어 넣어주고 해당 고 루틴 관리에 대해 생각보다 많은 고민을 했고,

이렇게 ERD를 작성했을 때 어떤 걸 기준으로 조회가 많이 발생할 것 같은데? 복합 PK 가 좋을까? 이런 고민은 필요한 것이 아닌가에 대해 팀장님과 의논을 했고 팀장님이 원하던 것은 완벽한 ERD, API 가 아닌 빠르게 먼저 기초 틀을 잡길 원하셨던건데 나는 다른 방향을 가고 있었다고 하셨다. 단순 자전거 수리점에서 체인만 갈면되는것을 , 이것저것 핸들 안장 바퀴 등을 다손보는 작업을 하고 있어 팀장님 입장에서는 답답하셨다고 한다.

 

테이블의 설계와 pk fk 전략은 기존 데이터 베이스와 동일하게 가져갔다. fk는 없이 id 칼럼을 추가하고 인덱스를 걸어주기로 했다.

지난 프로젝트에서 이렇게 설정했던 이유는 fk에 따른 조인 시 오버헤드 연산 그리고 msa 가 적용되어 있어 id 하나의 값을 가지고 레디스, mysql  한 번에 조회하는 이점 등이 있어 이렇게 설정했었다. 이에 대해 추후 어떻게 하면 이런 방식이 좋을 수 있는지에 대해 작성해 보겠다. 

그외 테이블 설계의 특별한것은 없었던것 같다.

 

ERD,API 명세서 작성이 완료된 이후 코드작성을 하는데 너무 재밌었다. 새로운 신규기능에 대해 개발 을 하는 것이 너무 새로웠다. 물론 프로젝트 구조상 api 핸들러는 템플릿 메서드 패턴이 적용되어 필요한 인터페이스를 구현하기만 하면 되지만 이런 api 가 아닌 라이브 채팅 서버와 채팅 매니저 관리 쪽은 고 루틴과 채널을 이용해야 했다.

이번에 배운 동시성 프로그래밍 의 규칙에 맞춰 잘 작성했다고 생각한다. 라이브 채팅 서버 혹은 vod에서 투표가 진행된다면 해당 결과를 통계테이블에 옮겨주는 작업이다. 물론 배치를 통해서 작성하면 손쉽게 작성할 수 있겠지만 투표진행이 없음에도 배치가 도는 비효율적인 코드를 작성하기 싫어 고 루틴과 채널을 활용해서 작성해 보았다.

 

캡슐화를 통해 고 루틴을 생성하는 함수에서 채널을 생성해 해당 채널을 반환하는 함수를 작성했다. 이에 따라 단방향 채널의 반환값이 결정되고, 함수의 호출 부는 해당 단방향 함수에서 값을 읽어오기만 하면 된다.

이렇게 작성된 코드는 파이프라인 의 패턴으로 확장하기도 쉬울뿐더러 단방향 채널의 반환값이 주는 매력에 빠졌다.

이에 따라 함수 호출자는 동시성에 대한 고려 없이 그냥 함수를 호출만 하면 되고, 함수 의 내부로직에서 동시성의 관리만 해주면 된다.

또한 해당 고 루틴 들은 각자 투표의 아이디 값을 기준으로 map을 통해 관리되고 map에서 삭제되는 순간에는 mutex lock을 걸어 동시성 이슈를 제거하고자 했다. 이렇게 하다 보니 차라리 투표의 핸들러? 를 만들어서 구조체 하나에서 관리하는 것이 더 좋다고 생각해 apiHandler의 구조체에 추가해 주었다.

최근 동시성 프로그래밍 책을 읽고 막 책에서 소개해주는 복잡하고, 어려운 패턴을 적용한 것이 아닌 동시성 프로그래밍에서 강조하는 컨택스트의 관리를 왜 함수 내부에서 관리하는 것이 좋은지 코드를 작성하면서 알게 되었다. 정말 코드의 작성이 간결하고 로직의 구현이 쉬웠다.

 

그러나 한 가지 고민되는 부분은 통계 테이블로 옮기는 과정이 upsert이다.

나의 설계에 따르면 투표가 진행 중이라면 1초에 한 번씩 고 루틴 신호가 발생해 upsert를 진행하는데, mysql 유후 커넥션 풀이 10개 필요시 20개까지 늘어나게 된다.

그렇다면 20개 모두 사용 중이라면 해당 선전됨 고 루틴은 하나의 upsert 가 진행되는 동안 나머지는 모두 대기상태에 빠져 생각보다 성능이 나쁘지 않을까 생각된다.

하지만 우리의 투표시스템이 막 100~200 개처럼 엄청 크다고 생각하지는 않는다. 만약 실제로 이렇게 진행되는 투표가 많이 존재한다면 차라리 배치 프로세스를 도입하는 것이 훨씬 이득인 것처럼 보인다. 

 

또한 실시간 채팅 의 경우 투표의 시작 메시지에 대해 브로드 캐스팅이 필요하다 현재 투표가 진행되었다는 브로드캐스팅이 필요하다.

우리의 서버는 채팅 매니저- 나츠 - 채팅 이 존재하는데 나츠는 고로 만들어진 메시지큐 이다. 어드민 에서 발생되는 메세지 들은 고 루틴 의버퍼링 채널을 통해 5~10개씩 나츠로 밀어 넣어주고 나츠에서는 이를 브로드캐스팅으로 채팅 접속자에게 나눠주는 구조로 되어있다.

 

이에 우리 채팅매니저 쪽에서는 밀어 넣어주는 메시지에 대해서 시그널에 대해 정의가 되어있는데 기존 것을 활용하기 에 적합한 곳이 없어 투표라는 시그널을 뚫어주었다. 

추후 나츠를 이용한 채팅을 간략하게 구현하는 것을 포스팅해보겠다.

 

확실히 CRUD 쪽에 있어서는 4~5월 프로젝트할 때보다 상당히 많은 시간이 줄어든 것 같다. orm에 익숙해지고 어떤 쿼리가 나갈지 알고 있으니 코드작성에 막힘없이 작성했으며, 사실 테스트 코드는 작성하지 않았다. 테스트 코드라기보다는 그냥 저 채팅 시뮬레이터 같은 것을 만들어서 신호에 따라 쿼리가 나가고 데이터 업데이트가 잘 되는지에 대해서 만 시뮬레이션을 돌렸다.

 

이번 개발 역시 시간이 부족했다. 여름휴가 기간이다 보니 다들 휴가를 가고, 휴가 복귀 후에 테스트를 원하시니... 적어도 8월 초에는 마무리를 지어야 하지 않겠는가?... 

실제 개발한 API는 17개 이고 현재 진행 중인 고 루틴에 대해 확인하는 테스트용 api까지 총 18개를 작성했다. 3개를 제외한 나머지는 단순 CRUD 여서 코드가 많았지 생각보다 고민시간 없이 수월하게 작성했으며 3개는 라이브와 고 루틴의 생성과 삭제에 연관되어 있다 보니 생각보다 고민을 많이 하면서 작성했다.

 

 

 

 

 

'개발일지' 카테고리의 다른 글

FRP 적용  (0) 2024.12.31
10월 개발일지  (1) 2023.11.01
6월 개발  (1) 2023.07.09
5월 개발  (0) 2023.06.11

5장은 추후 정리해서 올리고자 한다.

 

"동시성을 지원하는 언어의 장점" , OS 스레드 의 다중화를 위해 고 컴파일러는 "작업 가로채기" 전략을 사용한다 (work-strealing)

작업 가로채기 전략에 대해 알아보자.

 

1. 직관전인 스케쥴링 방법

공정한 스케쥴링을 상식적으로 한번 적용해 보자. 사용가능한 프로세스에 작업들을 나누어서 할당한다. 가장 이상적이고 직관적인 방식이다.

수행하는 프로세스 가 n 개 와 x 의 작업이 있다면 각 프로세스는 x/n 만큼 할당해서 작업을 수행하면 된다.

 

Go의 동시성 전략은 "fork-join 방식이다."

 

fork-join 모델 은 단순 어느 시점에서 분기가 나누어지며 미래의 특정 어느 합류지점이 생겨 메인 작업에 합류하는 것을 의미한다. 이러한 모델이 사용된다면 작업들이 서로 의존적일 확률이 매우 높아지게 되고 위에서 제시한 공정한 스케쥴링 방식에는 상당한 문제점이 발생된다.

P1, P2의 프로세스가 존재하고 해당 프로세스에서 각각 작업 a, b, c, d를 한다고 가정해 보자.

시간 P1 P2
  a b
n (대기) b
n+k (대기) c
n+k+m d 대기

이렇게 대기하는 시간이 생각보다 많이 발생하게 된다. 이를 해결하기 위해 fifo 대기열을 적용해서 중앙 집중식으로 구성해서 본인이 가능할 때 작업을 빼가는 방식을 적용하는 것이다.

 

2. 중앙 집중식 대기열

 

각 프로세스는 처리용량이 허용될 때 대기열에서 작업을 꺼내오고 그렇지 않으면? 조인에서 대기하고 있는다. 이는 1에서 제공하는 단순한 작업을 나누는 것보다는 나은 방법을 제공해 준다. 

하나 이에 대해 문제점이 다시 존재한다. 중앙대기열이라는 임계지점을 계속 들락날락해야 하기 때문에 이에 해당하는 비용이 든다.

뿐만 아니라 캐시의 지역성에도 문제가 발생해 캐시 미스가 자주 발생할 수 있는 가능성이 농후해진다.

 

3. 작업 대기열을 분산시키는 방법 - 작업 가로채기

위와 같이 프로세스가 자체적으로 큐를 가지는데 해당 큐는 dequeue 성질을 가지고 있다. 

고의 런타임은 포크조인 모델 방식을 따른다. 따라서 포크가 발생되는 지점이 생긴다면? 해당 스레드와 관련된 데큐의 끝 즉 꼬리에 추가된다.

 

이후 대기 중인 스레드가 작업을 가로채기를 할 때는 아래와 같은 규칙을 따르게 된다.

1. 스레드가 현재 처리할 작업이 없을 때 다른 스레드의 데큐에서 작업을 가로챈다.

2. 대기 중인 스레드는 주로 다른 스레드 데큐의 앞쪽(머리)에서 작업을 가로챈다.

 

스레드의 데큐 양쪽 이 모두 비어있는 경우

1. 조인을 지연시킨다.

- 지연을 시킴에 따라 2번 즉 다른 스레드의 데큐 앞쪽을 가로챌 수 있다.

- 합류에 대한 오버헤드를 최소화하기 위해 조인을 지연시킨다.

2. 임의의 스레드 데큐의 앞쪽 작업을 가로챈다.

 

코드로 표현해 보자.

func Test_runtime_go_01(t *testing.T) {
	var fib func(n int) <-chan int
	fib = func(n int) <-chan int {
		result := make(chan int)
		go func() {
			defer close(result)
			if n <= 2 {
				result <- 1
				return
			}
			result <- <-fib(n-1) + <-fib(n-2)
		}()
		return result
	}
	fmt.Printf("fib(4) result : %d ", <-fib(4))
}

이 프로그램이 두 개의 단일코어 가 존재하는 가상머신에서 돌아간다고 가정해 보자.

프로세스 1에 대해 T1, 프로세스 2에 대해 T2를 할당한다고 가정해 보자.

T1 호출스택 T1작업데큐 T2 호출스택 T2작업데큐
main 고루틴      
main 고루틴 fib(4)    
main 고루틴 합류지점
fib(4) -> 가로채기 성공
     
main 고루틴 합류지점
fib(4) -> 가로채기 성공
fib(3)
fib(2)
   
main 고루틴 합류지점
fib(4) -> 가로채기 성공
fib(2) fib(3)  
main 고루틴 합류지점
fib(4) -> 합류지점
fib(2) -> return 1
  fib(3)
fib(1)
main 고루틴 합류지점
fib(4) -> 합류지점
fib(2) from t2 -> return 1
  fib(3) -> 합류지점
fib(1) -> return 1


main 고루틴 합류지점
fib(4) -> 합류지점
  return 2  
main 고루틴 합류지점
fib(4) -> return 3
     
return 3      

서로의 스택에서 합류지점이 발생할 때 각각의 데큐에서 꺼내가는 것이 핵심 포인트이다.

 

4. 작업-연속 가로채기

고에서는 위에서 제시되는 일반적인 가로채기 알고리즘이 아닌 연속 가로채기 알고리즘을 구현하고 있다.

위에서 제시된 방법은 스레드가 쉬지 않고 돌아가는 장점이 있다 다만 지속적인 합류의 지점이 발생해 조인을 위한 지연이 발생된다. 

이 문제를 적절하게 해결하기 위해 연속 가로채기 알고리즘을 적용한다.

 

연속 가로채기 란 

기본적으로 가로채기의 개념을 확장한 내용이다. 하나의 작업이 아닌 연속 부분을 가로채는 것이다.

 

여기서 연속이란? 

포크 조인 모델 에는 2가지 옵션 작업, 연속의 개념이 존재한다.

작업 : 포크과정에서 분리되어 병렬로 실행되는 단위, 독립적으로 수행되는 특징

연속:  작업이 끝나고 수행되어야 하는 추가적인 단계나 계산을 의미 "두 하위 작업의 결과를 합쳐서 최종결과를 얻는 과정"

 

위와 동일한 작업을 표로 다시 한번 알아보자.

T1 호출스택 T1 작업데큐 T2 호출 스택 T2 작업데큐
main      
fib(4) main 연속    
fib(4)   main 연속(T1 작업데큐 가로챔)  
fib(3) fib(4) 연속    
fib(3)   main 연속 대기
fib(4) 연속 (T1 작업데큐 가로챔)
 
fib(2) fib(3) 연속 main 연속 대기
fib(2) (fib -2 두번째 연산)
fib4 연속
return 1   main 연속 대기
return 1
 
return 1 + fib(1)   main 연속 대기
fib4 연속
 
return 2   main 연속
fib4 연속
 
    main 연속
return 3 (T1의 결과 t2 의결과)
 
    main(3)  

이렇게 변경된 작업 가로채기는 T1의 호출스택을 보면 마치 함수의 콜스택 처럼 되어 있다 T1의 콜체인은

fib(3) fib(2) fib(1) 이 되는 방식을 주목해서 보면 되겠다.

 

이렇게 변경된 작업가로채기 에는 연속되어 있기에 실힁 순서가 보장되며, 합류지점의 지연이 없다는 장점이 존재한다.

기존 가로채기에서는 호출스택, 작업 중인 스레드의 지연된 조인 횟수 가 연속 가로채기보다 많이 존재한다.

 

해당장의 실제 페이지는 상당히 적지만 너무 추상적인 내용들이라 수십 번을 읽어보아야만 했다.

고 루틴이 작업 가로채기 방식을 적용하면서도 스레드 간의 합류 비용 즉 조인의 지연을 없애기 위해 확장한 개념 연속된 가로채기와 

왜 다른 언어에서는 할 수 없는지(go 의 컴파일러 때문에 가능함). 등에 대해 알 수 있었다.

 

위에 제공된 모든 혜택들을 우리는 고퍼이기에 go keyword를 통해  가장 효율적인 방식으로 작업이 자동으로 스케쥴링된다.

쉬는 날이기에 근 1~2주 동안 일찍 출근해서 풀었던 문제와 못 푼 문제에 대해 정리해보고자 한다.
물론 다시풀어보면서 작성해보고자 한다.

1. 백준 일곱 난쟁이 [2309]

아홉 개의 숫자를 주고 그중에서 일곱 난쟁이를 찾는 문제이다. 이중 일곱 난쟁이의 키의 합은 100이라는 전제조건이 붙으며 정답이 여러 개 존재할 경우 아무거나 하나출력하면 된다.

func solution() {
	reader := bufio.NewReader(os.Stdin)

	arr := make([]int, 9)
	for i := range arr {
		var a int
		fmt.Fscanln(reader, &a)
		arr[i] = a
	}

	sort.Ints(arr)
	btc := func(arr, rs []int, idx, sum int) {}

	btc = func(arr, rs []int, idx, sum int) {
		if len(rs) > 7 || sum >= 100 || idx >= len(arr) {
			if sum == 100 {
				for _, v := range rs {
					fmt.Println(v)
				}
				os.Exit(0)
			}
		} else {
			btc(arr, append(rs, arr[idx]), idx+1, sum+arr[idx])
			btc(arr, rs, idx+1, sum)
		}
	}

	btc(arr, []int{}, 0, 0)
}

백트리킹 방식을 사용했다. 난쟁이를 선택한 경우와 그렇지 않은 경우

최초 정렬을 수행하고 다음 항목을 진행하는데 정답의 출력이 오름차순 출력이기에 최초 입력값 자체를 정렬해 주었다.

 

이 방법 외에도 간단하게 이중 포문을 돌려서 하나씩 제거하는 방법도 존재한다.

func solution() {
	reader := bufio.NewReader(os.Stdin)

	arr := make([]int, 9)
	var (
		total, x, y int
	)
	for i := range arr {
		var a int
		fmt.Fscanln(reader, &a)
		arr[i] = a
		total += a
	}

	sort.Ints(arr)
Loop:
	for i := 0; i < len(arr); i++ {
		for j := i + 1; j < len(arr); j++ {
			if total-(arr[i]+arr[j]) == 100 {
				x, y = i, j
				break Loop
			}
		}
	}

	for i := range arr {
		if i != x && i != y {
			fmt.Println(arr[i])
		}
	}
}

이렇게 수를 받아오면서 전체 수의 값을 계산하고 이중포문으로 하나씩 날려주면 된다. 

 

2. 백준 로또 [6603]
1~49의 숫자 중 6개를 고르는 것이다 단 집합 S를 만들고 해당 수의 번호만 선택하는 전략을 가져간다.

해당 부분집합은 총 6~13 개의 숫자로 되어있으며, 입력마지막 줄에는 0이 하나 주어진다.

 

package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func main() {
	solution()
}

func solution() {
	reader := bufio.NewReader(os.Stdin)
	answer := strings.Builder{}

	bitmask := func(arr, rs []string, flag, start int) {}
	bitmask = func(arr, rs []string, flag, start int) {
		if len(rs) >= 6 {
			ans := ""
			for _, v := range rs {
				ans += v + " "
			}
			answer.WriteString(ans + "\n")
			return
		} else {
			for i := start; i < len(arr); i++ {
				if flag&(1<<i) != 0 {
					continue
				}
				bitmask(arr, append(rs, arr[i]), flag|(1<<i), i+1)
			}
		}
	}

	for {
		line, _ := reader.ReadString('\n')
		if line[0] == '0' {
			break
		}
		input := strings.TrimSpace(line)
		target := strings.Split(input, " ")[1:]
		var rs []string
		bitmask(target, rs, 0, 0)
	}
	fmt.Println(answer.String())
}

해당 문제는 비트마스크를 이용해 해결했다. 여기서 비트마스크는 & | 연산을 활용하는 방법이다. 

최초 0의 플래그를 가지고 이동하며 플래그 즉 이진수의 각 자릿수는 배열의 방문을 표기한다. 

 

예를 들어 0의 플래그가 있고 최초방문 0의 자리를 방문한다고 했을 때 0 | (1<<0) 1을 0만큼 쉬프트 한다.  즉 1과 0을 or 연산을 하게 되고플 래그는 0000001의 값을 가진다. 이룰 활용한 방법이다. 

나의 맥북은 64비트 메모리를 사용하고 있으니 int는 int64 고되고 이는 총 64개의 방문배열을 처리할 수 있음을 의미하니 마음 놓고 사용해도 된다.

 

지난번도 그렇고 이번에 다시 풀 때도 그렇고 input을 읽어올 때 "\n" 읽어오고 trimspace를 안 해 자꾸 출력값이 이상해서 오답을 받았다.

ㅎㅎ....

 

3. 부분수열의 합 [1182]

총 20 개의 숫자와 하나의 숫자가 주어지고 몇몇의 숫자들을 선택해 합을 구해 주어진 하나의 숫자와 같은 경우의 개수를 출력해야 한다.

여기서 부분수열의 합이라서 연속된 배열의 부분수열의 합이라고 생각할 수 있다. 그게 아닌 특정 숫자들을 선택해 더하는 것이다.

func solution() {
	reader := bufio.NewReader(os.Stdin)

	var (
		n, answer, sum int
	)

	fmt.Fscanln(reader, &n, &sum)

	arr := make([]int, n)
	for i := range arr {
		var a int
		fmt.Fscan(reader, &a)
		arr[i] = a
	}

	find := func(arr []int, total, idx, flag int) {}
	find = func(arr []int, total, idx, flag int) {
		if idx >= len(arr) {
			if total == sum && flag > 0 {
				answer++
			}
			return
		} else {
			find(arr, total+arr[idx], idx+1, flag|(1<<idx))
			find(arr, total, idx+1, flag)
		}

	}
	find(arr, 0, 0, 0)
	fmt.Println(answer)
}

1번 백설공주  와 마찬가지로 숫자를 선택 혹은 넘기는 쪽을 선택했다. flag를 넣어준 이유는 총합이 0 이 되는 경우 answer는 0으로 초기화되기 때문에 최소 고른 횟수를 넣어주기 위해 위와 같이 넣어주었다.

 

4. 차이를 최대로 [10819] 

범위 또한 3~8로 순열 완탐에 최적화된 전형적인 순열 문제이다. 각 배열을 바꿔서 차이를 최대로 만들면 되기 때문에 순열을 이용해서 재배치 후 계산을 해주면 된다. 

func solution() {
	reader := bufio.NewReader(os.Stdin)
	abs := func(a int) int {
		if a < 0 {
			return -a
		}
		return a
	}
	cal := func(arr []int) (total int) {
		for i := 1; i < len(arr); i++ {
			total += abs(arr[i] - arr[i-1])
		}
		return total
	}

	var (
		n, answer int
	)

	fmt.Fscanln(reader, &n)

	arr := make([]int, n)
	for i := range arr {
		var a int
		fmt.Fscan(reader, &a)
		arr[i] = a
	}
	answer = cal(arr)

	recur := func(arr, rs []int, depth, flag int) {}
	recur = func(arr, rs []int, depth, flag int) {
		if depth >= len(arr) {
			sum := cal(rs)
			if answer < sum {
				answer = sum
			}
			return
		} else {
			for i := 0; i < len(arr); i++ {
				if flag&(1<<i) == 0 {
					recur(arr, append(rs, arr[i]), depth+1, flag|(1<<i))
				}
			}
		}
	}
	recur(arr, []int{}, 0, 0)
	fmt.Println(answer)
}

단순히 비트마스크를 이용해서 순열을 조회해서 다음으로 넘겨준다. 이게 전부다.

 

5. 스도쿠 [2580] 

예전에 풀어봤던 문제이다. 확실히 한번 풀어봐서인지 나름 손쉽게 풀 수 있었다.
(https://guiwoo.tistory.com/3 - 자바풀이)

백트리킹의 조건과 패스 될 수 있는 수의 경우만 명확하게 해 주면 된다.

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	solution()
}

func solution() {
	reader := bufio.NewReader(os.Stdin)
	arr := make([][]int, 9)
	for i := range arr {
		sub := make([]int, 9)
		for j := range sub {
			var a int
			fmt.Fscan(reader, &a)
			sub[j] = a
		}
		arr[i] = sub
	}
	isPossible := func(arr [][]int, num, row, col int) bool {
		//가로줄 체크
		for i := 0; i < 9; i++ {
			if arr[row][i] == num {
				return false
			}
		}

		//세로줄 체크
		for i := 0; i < 9; i++ {
			if arr[i][col] == num {
				return false
			}
		}

		// 3*3 체크
		row = (row / 3) * 3
		col = (col / 3) * 3
		for i := row; i < row+3; i++ {
			for j := col; j < col+3; j++ {
				if arr[i][j] == num {
					return false
				}
			}
		}
		return true
	}

	dfs := func(arr [][]int, cal int) {}
	dfs = func(arr [][]int, cal int) {
		if cal >= 81 {
			for i := range arr {
				for j := range arr[i] {
					fmt.Printf("%d ", arr[i][j])
				}
				fmt.Println()
			}
			os.Exit(0)
		}
		row := cal / 9
		col := cal % 9
		if arr[row][col] != 0 {
			dfs(arr, cal+1)
		} else {
			for i := 1; i <= 9; i++ {
				if isPossible(arr, i, row, col) {
					arr[row][col] = i
					dfs(arr, cal+1)
					arr[row][col] = 0
				}
			}
		}
	}
	dfs(arr, 0)
}

이문제는 row, col을 하나의 숫자로 넘겨서 계산해 주는 게 보다 손쉽게 계산할 수 있다. 

 

6. 단어나누기 [1251]

알파벳 소문자 가 주어지고 단어를 임의의 두 부분을 골라서 단어를 쪼개고 나눠진 단어들을 뒤집어서 다시 재조합해 사전에서 찾을 수 있는 가장 앞에 단어를 출력해야 한다.

단 나눠진 단어의 길이는 1 이상인 단어여야만 한다.

 

포인트는 임의의 두 부분을 골라서 단어를 나눈다는 것이다.

바로 슬라이스를 사용하면 된다. 예를 들어보자 "abcdefg"와 같은 단어가 있다면? 

func Test_StringSplit(t *testing.T) {
	a := "abcdefg"
	fmt.Println(a[:1]) // a
	fmt.Println(a[1:3]) // bc
	fmt.Println(a[3:]) // defg
	fmt.Println(a[7:]) // 
}

 

단어의 특정 부분을 고르는 것이 아닌 모든 부분을 사용해야 하기 때문에 딱 2개의 숫자만 고르면 된다. 단 길이가 7이라면 6까지 골라야 한다. 위에서 언급한 나눠진 단어는 길이가 최소 1 이상이어야 하기 때문에

func solution() {
	reader := bufio.NewReader(os.Stdin)
	reverse := func(s string) (ans string) {
		for i := len(s) - 1; i >= 0; i-- {
			ans += string(s[i])
		}
		return ans
	}
	strCompare := func(a string, b string, n []int) string {
		b1 := reverse(b[:n[0]])
		b2 := reverse(b[n[0]:n[1]])
		b3 := reverse(b[n[1]:])
		if a > (b1 + b2 + b3) {
			return b1 + b2 + b3
		}
		return a
	}

	var input string
	answer := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
	fmt.Fscanln(reader, &input)

	recur := func(n int, rs []int, start, flag int) {}
	recur = func(n int, rs []int, start, flag int) {
		if len(rs) == 2 {
			answer = strCompare(answer, input, rs)
			return
		} else {
			for i := start; i < n; i++ {
				if flag&(1<<i) == 0 {
					recur(n, append(rs, i), i+1, flag|(1<<i))
				}
			}
		}
	}
	recur(len(input), []int{}, 1, 0)
	fmt.Println(answer)
}

string을 reverse 해주는 함수가 없어 따로 작성했다(그립다 java stringBuilder) answer 같은 경우는 최대 50 개의 단어가 주어지기 때문에 위와 같이 초기화시켜주었으며 조합을 응용해서 작성했다.

 

7. 부등호 [2529]

<,>  두 종류 의 부등호가 주어지고 주어진 부등호의 관계를 만족시켜야 한다는 조건이 붙는다.

부등호 앞뒤로 넣을 수 있는 숫자는 0~9까지이며 숫자의 선택은 한 번씩 만 가능하다.

k의 범위가 2~9까지 이기 때문에 나올 수 있는 숫자는 총 10자리이며 0 또한 표기해주어야 하기 때문에 스트링으로 처음부터 표기하면서 넘어가자.

func solution() {
	reader := bufio.NewReader(os.Stdin)
	var a int
	fmt.Fscanln(reader, &a)
	arr := make([]string, a)
	for i := range arr {
		var b string
		fmt.Fscan(reader, &b)
		arr[i] = b
	}

	min, max := "9999999999", "0"

	recur := func(rs []string, depth, flag int) {}
	recur = func(rs []string, depth, flag int) {
		if len(rs) >= a+1 {
			result := strings.Join(rs, "")
			if min > result {
				min = result
			}
			if max < result {
				max = result
			}
			return
		} else {
			for i := 0; i <= 9; i++ {
				if depth == 0 {
					recur(append(rs, fmt.Sprintf("%d", i)), depth+1, flag|(1<<i))
				} else {
					if flag&(1<<i) == 0 {
						next := fmt.Sprintf("%d", i)
						switch arr[depth-1] {
						case "<":
							if rs[depth-1] < next {
								recur(append(rs, next), depth+1, flag|(1<<i))
							}
						default:
							if rs[depth-1] > next {
								recur(append(rs, next), depth+1, flag|(1<<i))
							}
						}
					}
				}
			}
		}
	}
	recur([]string{}, 0, 0)
	fmt.Printf("%s\n%s", max, min)
}

최소 최댓값을 선정을 최초 그냥 스트링으로 지정하였다. 이후 기존 백트래킹 방식과 유사하게 작성하였다. 

else 구문에는 switch 구문을 이요해 케이스의 분기를 나누어 주었다. 요즘 select 구문을 자주 사용하다 보니 switch 문이 조금 더 편한 기분이기도 하다. 

 

8. 연산자 끼워넣기 [14888]

숫자 2~11개 가 주어지고 연산자는 주어진 숫자-1 만큼 주어진다. 이에 따라 숫자의 최댓값, 최솟값을 구하라는 문제이다. 

자릿수가 최대 여도 11자리 부호연산자는 10개 총 필요한 연산자는 10! 이 될듯하기 때문에 완탐으로 때려도 된다.

 

위의 대부분의 완탐의 경우에는 주어지는 숫자에 대해 재귀를 돌았다면 이번에는 정해진 operation에 의해 재귀를 돌면 된다.

func solution() {
	reader := bufio.NewReader(os.Stdin)

	var a int
	fmt.Fscanln(reader, &a)

	arr := make([]int, a)
	for i := range arr {
		fmt.Fscan(reader, &a)
		arr[i] = a
	}
	oper := make([]int, 4)
	for i := range oper {
		fmt.Fscan(reader, &a)
		oper[i] = a
	}
	min, max := 1<<31, -(1 << 31)

	recur := func(idx, sum int) {}
	recur = func(idx, sum int) {
		if idx == len(arr) {
			if min > sum {
				min = sum
			}
			if max < sum {
				max = sum
			}
			return
		} else if idx == 0 {
			recur(idx+1, arr[idx])
		} else {
			for i := 0; i < len(oper); i++ {
				if oper[i] > 0 {
					switch i {
					case 0:
						oper[i]--
						recur(idx+1, sum+arr[idx])
						oper[i]++
					case 1:
						oper[i]--
						recur(idx+1, sum-arr[idx])
						oper[i]++
					case 2:
						oper[i]--
						recur(idx+1, sum*arr[idx])
						oper[i]++
					default:
						oper[i]--
						recur(idx+1, sum/arr[idx])
						oper[i]++
					}
				}
			}
		}
	}
	recur(0, 0)
	fmt.Printf("%d\n%d", max, min)
}

연산자의 개수가 0 이상일 때만 switch 문의 재귀를 타도록 만들었으며 재귀호출 이후에는 다시 ++ 를 해주어 모든 경우에 대한 탐색 을 하고 마지막 수의 값을 min, max에 따라 가져간다.

 

9. 라그랑주의 네 제곱수 정리 [3933]

못 푼 문제이다.ㅠ  시간초과 의 벽을 어쩌지 못하고 주어지는 숫자 n에 대하여 최대 4개의 자연수의 제곱의 합이 주어지는 숫자 n과 일치하는 여부를 찾는 문제이다.

기존방식과 동일하게 백트래킹 을 사용했으나 실패했다. 시간초과로.... 그래서 방문한 부분에 대해 처리를 해주려고 

visit [][][] 이렇게 사용했다가 메모리 플로우로 터져버렸다.

풀이를 참조해 보니 그냥 4중 for 문 돌리던가 아니면 dp를 사용한다.

func solution() {
	reader := bufio.NewReader(os.Stdin)
	ans := strings.Builder{}

	getAnswer := func(n int) int {
		var cnt int
		for i := 1; i*i <= n; i++ {
			if i*i == n {
				cnt++
				continue
			} else if i*i > n {
				break
			}
			for j := i; i*i+j*j <= n; j++ {
				if i*i+j*j == n {
					cnt++
					continue
				} else if i*i+j*j > n {
					break
				}
				for k := j; i*i+j*j+k*k <= n; k++ {
					if i*i+j*j+k*k == n {
						cnt++
						continue
					} else if i*i+j*j+k*k > n {
						break
					}
					for l := k; i*i+j*j+k*k+l*l <= n; l++ {
						if i*i+j*j+k*k+l*l == n {
							cnt++
							continue
						} else if i*i+j*j+k*k+l*l > n {
							break
						}
					}
				}
			}

		}
		return cnt
	}
	for {
		var n int
		fmt.Fscanln(reader, &n)
		if n == 0 {
			break
		}
		ans.WriteString(strconv.Itoa(getAnswer(n)) + "\n")
	}

	fmt.Println(ans.String())
}

디피로 푸는 방법도 있다.

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	//fmt.Println(1 << 15)
	solution()
}

func solution() {
	reader := bufio.NewReader(os.Stdin)
	ans := strings.Builder{}
	var dp [32768][5]int
	for i := 1; i*i < 32768; i++ {
		dp[i*i][1] = 1
		for j := i * i; j < 32768; j++ {
			dp[j][2] += dp[j-i*i][1]
			dp[j][3] += dp[j-i*i][2]
			dp[j][4] += dp[j-i*i][3]
		}
	}

	getAnswer := func(n int) int {

		var cnt int
		for i := 0; i < 5; i++ {
			cnt += dp[n][i]
		}
		return cnt
	}
	for {
		var n int
		fmt.Fscanln(reader, &n)
		if n == 0 {
			break
		}
		ans.WriteString(strconv.Itoa(getAnswer(n)) + "\n")
	}

	fmt.Println(ans.String())
}

이 테이블 dp [i][j]은 “수 i를 j개의 제곱수의 합으로 표현하는 방법의 수”를 저장한다.

초기에 i*i (제곱수 자체)는 i*i를 1개의 제곱수로 표현하는 방법이므로 dp [i*i][1]=1로 설정되고.
dp를 채우는 이 반복문에서, j는 새로운 합을 나타내는 인덱스이고 i는 제곱수를 나타냅니다. j에 대해 반복할 때마다 i의 제곱값을 현재의 j에 뺀 값 (j-i*i)에 대한 DP 값을 가지고 온 후, j*(i*i)에 해당하는 값을 경신합니다. 이렇게 하면, 각 j에 대해 최대 j 개의 제곱수로 수를 표현하는 방법의 수를 누적하여 저장할 수 있다.
즉, dp [j][2]는 j를 2개의 제곱수의 합으로 표현하는 경우의 수를 나타내며, dp [j][3]과 dp [j][4] 역시 동일한 방식으로 계산된다.

 

현실적으로 이 방법에 대해 어렴풋이 느낌만 있었지 구현하라 그러면 못하지 않았을까 싶다... 

 

10. 스타트와 링크 [14889]

arr로 각  번호들의 능력치를 입력 값으로 주고 arr [i][j] + arr [j][i]는 같은 팀이 된다면 앞에와 같은 힘을 낸다 따라서 조합으로 팀원을 2개의 팀으로 구분 짓고 각각의 팀의 능력치의 최소 값을 찾아주면 된다. 

한 명의 플레이어가 선택된다면 그건 가로 세로 다 선수의 능력치를 사용할 수 없다 다시 말해 저 2차원 배열은 1차원 배열로 다시 나열될 수 있다.

위 그림에서 해당 인원들의 능력치를 다시 1차원 배열로 변경한다면 

[6 15 10 12] [14 6 12 11]  이렇게 변경될 수 있다. 이  모든 수의 합은 43이 되고 이는 모두 중복되는 값이다. 여기서 1,4를 선택한다고 하면 1(6+14), 4(12+11) 이 들어가고 total에서 빼주면 반대편 값이 나온다.

이해가 잘 안 된다면 풀어서 작성해 보자 중복된 값들의 덧셈이 존재해 이것이 가능하다.

 

1번 (6(1+2+3) + 14(4+7+3)) 4번 (12(3+4+5) + 11(3+6+2))

2번 (15(4+5+6) + 6(1+1+4)) 3번 (10(7+1+2) + 12(2+5+5))

 

위에 보이는 것처럼 중복된 값들이 계산되기 때문에 모두 더한 후 total에서 빼주게 되면 값이 일치하게 된다.

 

func solution() {
	reader := bufio.NewReader(os.Stdin)
	abs := func(a int) int {
		if a < 0 {
			return -a
		}
		return a
	}

	var a int
	fmt.Fscanln(reader, &a)

	arr := make([][]int, a)
	row := make([]int, a)
	col := make([]int, a)
	total := 0
	for i := range arr {
		sub := make([]int, a)
		for j := range sub {
			var b int
			fmt.Fscan(reader, &b)
			sub[j] = b
			total += b
			row[i] += b
			col[j] += b
		}
		arr[i] = sub
	}
	min := 1 << 31
	recur := func(start, depth, sum, flag int) {}
	recur = func(start, depth, sum, flag int) {
		if depth == a/2 {
			target := abs(total - sum)
			if target < min {
				min = target
			}
			return
		} else {
			for i := start; i < a; i++ {
				if flag&(1<<i) == 0 {
					recur(i+1, depth+1, sum+row[i]+col[i], flag|(1<<i))
				}
			}
		}
	}
	recur(0, 0, 0, 0)
	fmt.Println(min)
}

사실 해당 문제의 다른 버전을 친구에게 제공받아 풀었지만 실패하고 이 문제를 찾아서 풀게 되었다.

 

11. 링크와 스타트 [15661]

해당 문제는 위에서 언급한 것처럼 해결하지 못했다.

위와 동일한 조건이지만 단 팀이 무조건 공평한 숫자가 아닌 최소 한 명 이상만 있으면 팀의 조건을 만족하게 된다.

위에서 제시한 조건을 그대로 이용해서 사용한다면?

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	//fmt.Println(1 << 15)
	solution()
}

func solution() {
	reader := bufio.NewReader(os.Stdin)
	abs := func(a int) int {
		if a < 0 {
			return -a
		}
		return a
	}
	min := func(a, b int) int {
		if a < b {
			return a
		}
		return b
	}

	var a int
	fmt.Fscanln(reader, &a)

	arr := make([][]int, a)
	row := make([]int, a)
	col := make([]int, a)
	total := 0
	for i := range arr {
		sub := make([]int, a)
		for j := range sub {
			var b int
			fmt.Fscan(reader, &b)
			total += b
			row[i] += b
			col[j] += b
		}
	}

	res := 1 << 31
	recur := func(idx, cur int) {}
	recur = func(idx, cur int) {
		res = min(res, abs(cur))
		for i := idx + 1; i < a; i++ {
			recur(i, cur-row[i]-col[i])
		}
	}

	recur(0, total)
	fmt.Println(res)
}

계산된 total 값을 이용해 한 칸씩 없애주면 된다. 오셀로 하듯이

 

나는 완탐을 잘못하기 때문에 이렇게 하루 쉬는날을 이용해서 풀었던 완탐을 정리해 봤다.

또 하루 날 잡고 완탐에 대해 다시 풀어보고자 한다.

완벽하게 완탕이 될 때까지 해보고 싶다.

 

'PS > Boj' 카테고리의 다른 글

백준-완탐 연습(Brute-Force)-2  (0) 2023.08.20
백준 1251 단어나누기[Java]  (0) 2022.06.26
백준 1120 문자열[Java]  (0) 2022.06.26
백준 10448 유레카[Java]  (0) 2022.06.26

템플릿 패턴에 대해 자료를 조사하고, 예제를 만들면서 느낀 점은 확실히 전략패턴과 많이 유사한 느낌을 가지고 있다.

팀장님의 손길이 닿아있는 프로젝트 라면 라우팅 하는 대부분의 부분은 이 템플릿 메소드 부분이 적용되어 있는데 

간략한 버전의 프레임워크를 작성해보고자 한다.

 

템플릿 메서드 패턴 이란 ? 

템플릿 메소드 패턴 은 소프트웨어 공학에서 동작 상의 알고리즘의 프로그램 뼈대를 정의하는 행위 디자인 패턴이다.
 알고리즘의 구조를 변경하지 않고 알고리즘의 특정 단계들을 다시 정의할 수 있게 해 준다. - wiki- 

템플릿 메서드는 부모 클래스에서 알고리즘의 골격을 정의하지만, 해당 알고리즘의 구조를 변경하지 않고 자식 클래스들이 알고리즘의 특정 단계들을 오버라이드​(재정의)​할 수 있도록 하는 행동 디자인 패턴입니다. -wiki-

기존 전략패턴 에 대해 공부했던 지난번의 기억을 살려보면 무언가 많이 의미가 비슷하다. 

"전략패턴" : 실행 중 알고리즘을 선택할 수 있게 하는 행위라고 지난번 위키에서는 정했다. 음? 설명이 엄청 비슷하다고 느껴진다. 

 

UML을 확인해 보자.

좌측 : 전략패턴 / 우측 : 템플릿 메소드 패턴

지난번 전략패턴을 구현하는 데 있어 우리는 객체에 Strategy 인터페이스를 주입받는 알고리즘 형태에 따라 기존 객체는 변경되는 형태를 가져갔다.

그러나 이번 템플릿 메서드 패턴은 상위 구현체 ? 에서 메소드를 사용하는데 그에 대해 상속된 객체는 해당 메소드를 오버라이드 해서 작성한다. OOP 언어 라면 위와같은 오버라이드 기능이 있겠지만? 우리 고퍼 들에게는 인터페이스 의 주입이 필요하다.

이는 코드 예제를 보면서 확인하자.

 

왜 템플릿 메소드 패턴이 생겼는가?

 

1. 여러 클래스가 거의 비슷하거나 같은 로직이나 연산을 수행하지만, 다소 차이점이 존재하는 경우
2. 중복 코드를 피하고 로직을 재사용하려는 경우
3. 알고리즘의 특정 단계를 반드시 오버라이드 해야 하는 경우

 

이유가 정말 명확하다. 중복되는 로직은 상위단계로 추상화하고 서로 다른 부분의 로직을 하위 상속객체에게 위임한다.

다시 말해 SOLID의 원칙 중 IOC 제어의 역전에 있어 엄청난 강점이 있다고 생각되는 부분이다. 왜?

하위 상속 혹은 오버라이드 하는 객체에서는 해당 함수의 호출 은 상위 클래스에서 전체 흐름을 제어하기 때문이다.

 

템플릿 메서드의 장점

1. 클라이언트들이 대규모 알고리즘의 특정 부분만 오버라이드하여 다른 부분에 변경하여 발생하는 영향을 덜 받도록 할 수 있다.

2. 중복 코드를 부모 클래스로 가져올 수 있다.

 

1번의 장점을 읽다 보면 어떤 개념들과 매우 유사하다. 바로 프레임워크이다. 프레임워크는 정해진 틀에 따라 우리가 작성한 코드들을 가져다 쓴다. 그래서 스프링 프레임워크를 공부하다 보면 특징 중 하나를 IOC라고 말한다. 왜? 프레임워크 이기 때문이다. 

물론 프레임워크 들은 템플릿 메서드 패턴 과 팩토리 메소드 추상팩토리 가 복합적으로 적용된 복잡한 틀이겠지만, 왜 이런 프레임워크들의 특징 중 하나로 IOC 가 나오는지 이해할 수 있으면 된다고 생각된다.

 

예제

고 에는 다양한 웹프레임워크 가 존재하지만 자체 http 패키지도 매우 훌륭한데 이 기본 패키지를 이용해서 나만의 작은 프레임워크를 작성해 보자.

개괄적인 콜스택과 구조를 그림으로 표현했다.

http.NewServeMux를 통해 개별적인 핸들러를 가진다. 해당 핸들러는 라우팅을 커스텀하게 할 수 있고

라우팅 할 때 서비스핸들러를 이용해 각각의 요청에 따른 프로세스를 진행시켜 준다. 코드를 확인해 보자.

package template

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

/**
무언가 만드는 방법이 유사하다.
*/

type ServiceTemplate interface {
	IsRequireAuth() bool
	GetRequest() error
	GetParam() error
	Process() error
}

type ServiceHandler struct {
	service ServiceTemplate
}

func (s *ServiceHandler) Init(service ServiceTemplate) *ServiceHandler {
	s.service = service
	return s
}
func (s *ServiceHandler) ParentsFeature01() {
	fmt.Println("부모의 기능01")
}
func (s *ServiceHandler) ParentsFeature02() {
	fmt.Println("부모의 기능02")
}

func (s *ServiceHandler) Run(w http.ResponseWriter, r *http.Request) {

	s.ParentsFeature01()
	s.ParentsFeature02()

	if s.service.IsRequireAuth() {
		fmt.Println("인증이 필요한경우 여기서 구현")
	}
	if err := s.service.GetRequest(); err != nil {
		fmt.Println("GetRequest 에러 처리")
	}
	if err := s.service.GetParam(); err != nil {
		fmt.Println("GetParam 에러 처리")
	}
	if err := s.service.Process(); err != nil {
		fmt.Println("Process 에러 처리")
	}
}

type ServiceGetTest struct{}

func (s ServiceGetTest) IsRequireAuth() bool {
	//TODO implement me
	panic("implement me")
}

func (s ServiceGetTest) GetRequest() error {
	//TODO implement me
	panic("implement me")
}

func (s ServiceGetTest) GetParam() error {
	//TODO implement me
	panic("implement me")
}

func (s ServiceGetTest) Process() error {
	//TODO implement me
	panic("implement me")
}

var _ ServiceTemplate = (*ServiceGetTest)(nil)

type ServicePostTest struct{}

func (s ServicePostTest) IsRequireAuth() bool {
	//TODO implement me
	panic("implement me")
}

func (s ServicePostTest) GetRequest() error {
	//TODO implement me
	panic("implement me")
}

func (s ServicePostTest) GetParam() error {
	//TODO implement me
	panic("implement me")
}

func (s ServicePostTest) Process() error {
	//TODO implement me
	panic("implement me")
}

var _ ServiceTemplate = (*ServicePostTest)(nil)

type MyContext struct {
	mux     *http.ServeMux
	service *ServiceHandler
}

func (m *MyContext) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	m.mux.ServeHTTP(w, r)
}
func (m *MyContext) InitRouting() {
	m.mux.HandleFunc("/get", m.service.Init(ServiceGetTest{}).Run)
	m.mux.HandleFunc("/post", m.service.Init(ServicePostTest{}).Run)
}

func ProcessService() {

	myHandler := &MyContext{
		mux:     http.NewServeMux(),
		service: &ServiceHandler{},
	}

	http.NewServeMux()
	myHandler.InitRouting()

	s := &http.Server{
		Addr:           ":9300",
		Handler:        myHandler,
		ReadTimeout:    5 * time.Second,
		WriteTimeout:   5 * time.Second,
		MaxHeaderBytes: 1 << 20,
	}

	log.Fatal(s.ListenAndServe())
}

ServiceTemplate라는 인터페이스를 정의하고, IsRequireAuth, GetRequest, GetParam, Process 같은 메서드들이 있다. 

이들은 템플릿 메소드 패턴에서 "템플릿 메서드"의 단계들이라 할 수 있다.

type ServiceTemplate interface {
	IsRequireAuth() bool
	GetRequest() error
	GetParam() error
	Process() error
}

 

ServiceHandler 타입은 ServiceTemplate 인터페이스를 멤버로 갖는 구조체다. ServiceHandler는 또한 초기화를 위한 Init 메서드와 ParentsFeature01, ParentsFeature02 (공통 기능을 구현하는 몇 가지 메서드), 그리고 Run 메서드를 가진다.
Run 메서드는 결국 템플릿 알고리즘을 실행하며, 각각의 인터페이스 메서드들을 호출한다.

type ServiceHandler struct {
	service ServiceTemplate
}

func (s *ServiceHandler) Init(service ServiceTemplate) *ServiceHandler {
	s.service = service
	return s
}
func (s *ServiceHandler) ParentsFeature01() {
	fmt.Println("부모의 기능01")
}
func (s *ServiceHandler) ParentsFeature02() {
	fmt.Println("부모의 기능02")
}

func (s *ServiceHandler) Run(w http.ResponseWriter, r *http.Request) {

	s.ParentsFeature01()
	s.ParentsFeature02()

	if s.service.IsRequireAuth() {
		fmt.Println("인증이 필요한경우 여기서 구현")
	}
	if err := s.service.GetRequest(); err != nil {
		fmt.Println("GetRequest 에러 처리")
	}
	if err := s.service.GetParam(); err != nil {
		fmt.Println("GetParam 에러 처리")
	}
	if err := s.service.Process(); err != nil {
		fmt.Println("Process 에러 처리")
	}
}


여기서 Init 은 포인터 자체를 반환하는데 빌더패턴의 체이닝 방식을 적용하기 위해서 위와 같이 선언하였다.

func (m *MyContext) InitRouting() {
	m.mux.HandleFunc("/get", m.service.Init(ServiceGetTest{}).Run)
	m.mux.HandleFunc("/post", m.service.Init(ServicePostTest{}).Run)
}

 

 

 

이렇게 간단한 예제를 작성했다.

 

전략패턴과 템플릿메서드 패턴의 명확한 차이가 여기서 드러난다. 또한 템플릿과 팩토리 패턴 의 밀접한 연관관계를 느낄 수도 있다. 

추상클래스 가 없는 고에서는 인터페이스를 임베딩 해서 사용해야 하는데 상당 부분이 팩토리메서드 패턴을 구현하는 부분과 유사하다.

 

위에 작성된 예제가 우리 팀장님이 작성한 코드에서 보이는 방식의 라우팅과 핸들러 함수 작성 방법이다.

이 템플릿 메서드의 단점 중 하나는 중간 인터페이스 즉 ServiceTempalte의 인터페이스 변경된다 면 엄청나게 많은 부분을 수정해야 한다. 이미 진행되었던 프로젝트 라면? 상당한 api 가 구현된 상태라면? 모조리 구현해주어야 한다. ㅠ

 

안 그래도 지난 개발에서 이 문제에 대해서 겪었고 bool 타입을 리턴해 훅? 비슷한 개념으로 하위에서 구현된 지 여부에 따라 상위 함수를 호출할지 아니면 구현체에서 호출할지 분기를 나누어 주었는데 상당히 고생했다.

 

디자인패턴을 공부하면 할수록 코드의 아키텍처 가 얼마나 중요한지 매번 느낀다. 

아키텍처에 따라 mock tdd의 여부도 가능할뿐더러, 이런 패턴의 적용 또한 분리 추상화가 손쉽게 가능하다. 그게 아니라면... 너무 많은 부분을 수정해야 하고 수정이 많으면 많을수록? 사이드 이펙트는 스노볼 마냥 커지고..

나중에는 감당하지 못해 롤백을...

 

 

 

+ Recent posts