반응형

문제

아래 <그림 1>과 같이 여러개의 정사각형칸들로 이루어진 정사각형 모양의 종이가 주어져 있고, 각 정사각형들은 하얀색으로 칠해져 있거나 파란색으로 칠해져 있다. 주어진 종이를 일정한 규칙에 따라 잘라서 다양한 크기를 가진 정사각형 모양의 하얀색 또는 파란색 색종이를 만들려고 한다.

전체 종이의 크기가 N×N(N=2k, k는 1 이상 7 이하의 자연수) 이라면 종이를 자르는 규칙은 다음과 같다.

전체 종이가 모두 같은 색으로 칠해져 있지 않으면 가로와 세로로 중간 부분을 잘라서 <그림 2>의 I, II, III, IV와 같이 똑같은 크기의 네 개의 N/2 × N/2색종이로 나눈다. 나누어진 종이 I, II, III, IV 각각에 대해서도 앞에서와 마찬가지로 모두 같은 색으로 칠해져 있지 않으면 같은 방법으로 똑같은 크기의 네 개의 색종이로 나눈다. 이와 같은 과정을 잘라진 종이가 모두 하얀색 또는 모두 파란색으로 칠해져 있거나, 하나의 정사각형 칸이 되어 더 이상 자를 수 없을 때까지 반복한다.

위와 같은 규칙에 따라 잘랐을 때 <그림 3>은 <그림 1>의 종이를 처음 나눈 후의 상태를, <그림 4>는 두 번째 나눈 후의 상태를, <그림 5>는 최종적으로 만들어진 다양한 크기의 9장의 하얀색 색종이와 7장의 파란색 색종이를 보여주고 있다.

입력으로 주어진 종이의 한 변의 길이 N과 각 정사각형칸의 색(하얀색 또는 파란색)이 주어질 때 잘라진 하얀색 색종이와 파란색 색종이의 개수를 구하는 프로그램을 작성하시오.

입력

첫째 줄에는 전체 종이의 한 변의 길이 N이 주어져 있다. N은 2, 4, 8, 16, 32, 64, 128 중 하나이다. 색종이의 각 가로줄의 정사각형칸들의 색이 윗줄부터 차례로 둘째 줄부터 마지막 줄까지 주어진다. 하얀색으로 칠해진 칸은 0, 파란색으로 칠해진 칸은 1로 주어지며, 각 숫자 사이에는 빈칸이 하나씩 있다.

출력

첫째 줄에는 잘라진 햐얀색 색종이의 개수를 출력하고, 둘째 줄에는 파란색 색종이의 개수를 출력한다.


주어진 조건에 맞을때 기존 역역을 4분할 하는 문제이며 이런 방식을 쿼드 트리 라고 한다.

경우의 수가 두개인 상황이라 분할정복 - 쿼드트리 문제의 입문이라고 볼 수 있겠다.

분할정복 알고리즘 ?
기본적으로는 엄청나게 크고 방대한 문제를 조금씩 조금씩 나눠가면서 용이하게 풀 수 있는 문제 단위로 나눈 다음 그것들을 다시 합쳐서 해결하자는 개념에서 출발하였다. 대표적으로는 퀵소트나  병합정렬이 있다.
출처 : 나무위키 - 분할 정복 알고리즘

 

사실 처음엔 큰 문제를 작게 쪼개서 계산?? 이 문제에 적용이 된다고? 싶었지만 처음 접해보는 분할정복 문제였어서 감이 안왔던듯 하다.

직접 문제를 손으로 써내려가면서 이해해 보았다.

 


문제에서 주어진 조건은 크게 두가지가 있다.

1. 주어진 영역 내 서로 다른 색이 존재 할 경우 해당 영역을 4분할 한다.

2. 정사각형 내 동일 색상 단위로 총 몇개 정사각형이 존재하는지 출력한다.

 

따라서 1번 조건에 맞을 경우 문제를 쪼개면 되는것이고, 쪼갠 상황에서 2번 조건에 맞는다면 답으로 기록하면 된다.

시작점은 [0, 0] 으로 한다. 시작점의 색상을 기억해 두고, 일정한 인접 행렬을 탐색하다가 다른 값이 나온다면 1번 조건에 부합하므로 즉시 4분할을 해준다.

 

분할된 영역의 구간은 규칙적으로 변경된다.

python의 // 는 값을 나눈 몫을 반환하므로 n값이 8일때 n//2를 하면 그 값은 4가 된다. 또한 python의 for loop에서 range의 end parameter는 해당 값 "미만"으로 동작하기 때문에 n//2 값을 넣어주면 된다.

 

처음에 탐색의 시작을 [0,0] 에서 한다고 하였으므로 첫번째 이중 for loop의 탐색 범위는 탐색x는 (이하 xx) x ~ x+n(미만) 이고, 탐색y는 (이하 yy) y ~ y+n 이 된다.

 

첫번째 cut 에서 또다시 서로다른 색이 발견되어 4분할을 진행해 줄때, x값과 y값의 범위 변화는 규칙적으로 변경된다.

위 규칙을 토대로 풀이를 작성하면 아래와 같다.

import sys
sys.stdin = open("input.txt")
n = int(input())
board = [list(map(int, input().split())) for _ in range(n)]

white = 0
blue = 0
def cut(x, y, n):
    global white
    global blue
    check_color = board[x][y]
    for xx in range(x, x+n):
        for yy in range(y, y+n):
            if board[xx][yy] != check_color: # 조건 1 부합 : 4분할 진행
                cut(x=x, y=y, n=n//2) # 1영역 : x = x ~ x+(n//2) | y = y ~ y+(n//2) | n = n//2
                cut(x=x, y=y+n//2, n=n//2) # 2영역 : x = x ~ x+(n//2) | y = y+n//2 ~ y+n//2+n | n = n//2
                cut(x=x+n//2, y=y, n=n//2) # 3영역 : x = x+n//2 ~ x+n//2+n | y = y ~ y+(n//2) | n = n//2
                cut(x=x+n//2, y=y+n//2, n=n//2) # 4영역 : x = x+n//2 ~ x+n//2+n | y = y+n//2 ~ y+n//2+n | n = n//2
                return # 분할은 분할대로 호출해놓고, 값에 적용되는걸 막기 위해 분할 뒤 return 한다.
    # 조건 2 부합 : 분할되지 않으면 모두 색이 같다는 것으로 정답으로 기록한다.
    if check_color == 1:
        blue += 1
    else:
        white += 1

cut(0, 0, n)  # 시작 조건 [0,0]
print(f"{white}\n{blue}")

 

큰 문제를 쪼갠다는 것과, 4분할 처리 하는 것에 대해 알 수 있었다.!

반응형
반응형

이분탐색법을 이용해 최소/최대 값으로 답이 결정되는 알고리즘을 풀어보자


2792 번

문제

보석 공장에서 보석 상자를 유치원에 기증했다. 각각의 보석은 M가지 서로 다른 색상 중 한 색상이다. 원장 선생님은 모든 보석을 N명의 학생들에게 나누어 주려고 한다. 이때, 보석을 받지 못하는 학생이 있어도 된다. 하지만, 학생은 항상 같은 색상의 보석만 가져간다.

한 아이가 너무 많은 보석을 가져가게 되면, 다른 아이들이 질투를 한다. 원장 선생님은 이런 질투심을 수치화하는데 성공했는데, 질투심은 가장 많은 보석을 가져간 학생이 가지고 있는 보석의 개수이다. 원장 선생님은 질투심이 최소가 되게 보석을 나누어 주려고 한다.

상자에 빨간 보석이 4개 (RRRR), 파란 보석이 7개 (BBBBBBB) 있었고, 이 보석을 5명의 아이들에게 나누어 주는 경우를 생각해보자. RR, RR, BB, BB, BBB로 보석을 나누어주면 질투심은 3이 되고, 이 값보다 작게 나누어 줄 수 없다.

상자 안의 보석 정보와 학생의 수가 주어졌을 때, 질투심이 최소가 되게 보석을 나누어주는 방법을 알아내는 프로그램을 작성하시오.

입력

첫째 줄에 아이들의 수 N과 색상의 수 M이 주어진다. (1 ≤ N ≤ 109, 1 ≤ M ≤ 300,000, M ≤ N)

다음 M개 줄에는 구간 [1, 109]에 포함되는 양의 정수가 하나씩 주어진다. K번째 줄에 주어지는 숫자는 K번 색상 보석의 개수이다.

출력

첫째 줄에 질투심의 최솟값을 출력한다.

 

예제1

[입력]     [출력]
5 2       3
7
4

 

예제2

[입력]    [출력]
7 5       4
7
1
7
4
4

이분탐색법은 주어진 정답 구간의 최소값과 최대값을 각각 left point, right point로 잡고, 그의 중간값을 확인하여 답에 맞는지 확인하며 찾아가는 방법이다. 기준이 되는 lt, rt의 중간값으로 왼쪽구간, 오른쪽 구간을 나누기 때문이 이분탐색이라고 한다.

 

위 문제에서 정답으로 요구하는것은 "질투수치" 이다. 질투수치는 보석을 나눠받은 학생들 중 최고로 많은 보석을 가져간 학생의 보석갯수를 의미한다. 이 값의 최소치를 구한다는 것은 곧, 학생들에게 보석을 최대한 적게 골고루 나누준다는 뜻이다.

 

위 문제에서 주어진 조건은

1. 학생은 한가지 색의 보석만 받을 수 있다.

2. 보석은 모두 나눠주어야 한다. (보석이 남으면 안됨) 그러나 보석을 아예 못받는 학생이 있을 수 있다. (5명중 3명만 보석받아도 괜찮음)

 

위 두가지 조건에 유의하며 코드를 작성해 보았다.

import sys
sys.stdin = open("input.txt")

students, color = map(int, input().split())
jewels = [int(input()) for _ in range(color)]

lt = 1  # 보석을 나눠받는 가장 작은 수 1
rt = max(jewels) # 보석을 나눠받는 가장 큰 수
while lt <= rt:
    mid = (lt + rt) // 2  # 중간값 탐색
    get_s = 0  # mid 갯수로 보석을 나눠받은 총 학생 수
    for j in jewels:
        if j % mid == 0:
            get_s += (j//mid) # 똑 나눠 떨어지면 몫이 곧 그 수만큼 나눠받은 학생 수
        else:
            get_s += (j//mid) + 1 # 안나눠떨어지면 몫에다가 나머지만큼 가져간 학생 수
            
    if get_s > students:
    	# 나눠받은 학생 수 가 주어진 학생수보다 많다면 더 많이씩 나눠줘야 한다.
        lt = mid + 1
    else:
        # 나눠받은 학생 수가 주어진 학생수보다 같거나 적다면 더 적게 나눠줘도 된다.
        rt = mid - 1
print(lt)  #

 

[변수] mid, get_s

- 이분탐색을 진행하면서 몇개의 보석으로 학생들이 나눠받을 것인지 mid 값을 정한다. 이 값은 항상 구간의 최소값과 최대값의 중간값이다. (2로 나눈 몫)

- mid 갯수만큼 보석을 나눠받을때, 한개 색상의 보석이 mid의 배수이면 그 몫 만큼의 학생수가 보석을 나눠갖는다.

- mid 갯수만큼 보석을 나눠받을때, 한개 색상 보석의 수가 mid로 나누었을때 나머지가 발생한다면 몫의 수 만큼의 학생이 mid값 만큼의 보석수를 나눠갖고, 한 학생은 나머지 수 만큼 보석을 나눠받게 되므로 몫 + 1 만큼의 학생들이 나눠받게 된다.

 

[계산하기] lt, rt

> lt = mid + 1

mid 갯수만큼 학생들이 보석을 나눠가질때, 나눠갖게된 학생의 수가 문제로 주어진 students의 수보다 크다면 이는 보석을 좀더 많이씩 나눠가져서 나눠받는 학생의 수를 줄여하 한다는 것을 의미한다.

따라서 mid 갯수만큼은 정답이 되지 않는것을 인지하고, 탐색 범위를 높이기 위해 최소값이었던 lt를 mid+1만큼으로 변경한다.

 

> rt = mid - 1

mid 갯수만큼 학생들이 보석을 나눠가질때, 나눠받은 학생의 수가 주어진 students의 수보다 같거나 적다면 일단 mid값은 정답이 된다는 것이다. 하지만 답에서 구하는것은 그 값의 최소값이기 때문에 더 작은 값도 정답이 될수 있는지 탐색해야 한다.

따라서 mid 값보다 1 작은수로 rt값을 변경하고 그 사이값들을 비교한다.

 

[정답 도출하기] lt

while 문을 빠져나오는 조건은 두가지가 있다.

1. lt와 rt의 값이 동일할때, lt 값이 mid+1 된 경우

2. lt와 rt의 값이 동일할때, rt 값이 mid-1 된 경우

 

먼저 1번의 경우 새로운 lt값이 된 mid+1값은 언젠가 그 값이 정답이었던 mid 값이다. 따라서 while 조건에서 벗어나게 되어 더이상 탐색이 불가능하게 되더라도 lt값은 이전에 정답으로 체크되었던 mid 값이었으므로 정답이 된다.

 

2번의 경우 이미 2번의 조건이 정답에 부합하는 조건이므로 그 당시의 lt값 (이때는 lt와 rt가 같지만 한번 더 탐색을 위해 rt값이 줄어들어버린것) 을 정답으로 확정한다.

 


주어진 예제 1번을 확인해보면 [정답 도출하기]의 2번 상태에서 while 문이 종료된다. 예제2번의 경우 1번 상태에서 while 문이 종료되는데 풀이를 확인해보면 아래와 같다.

 

 


15810번

위와 동일한 개념으로 문제를 풀면 된다.

import sys
sys.stdin = open("input.txt")

staff, ba = map(int, input().split())
times = list(map(int, input().split()))

lt = 1
rt = max(times) * ba

while lt <= rt:
    mid = (lt + rt) // 2
    # 총 소요시간이 mid일때 몇개까지 풍선을 불 수 있는가
    total_balloon = 0
    for one in times:
        # 불고나서 몇분 남았다 해도 그 시간안에 부는것 완성 못하므로 나머지는 버린다.
        total_balloon += mid//one
    if total_balloon >= ba:
        # 목표갯수와 같거나 보다 많다면 총 소요시간을 줄여본다
        rt = mid - 1
    else:
        # 목표갯수보다 모자르다면 총 소요시간을 늘려준다
        lt = mid + 1
print(lt)

 

최소 / 최대 값이 답으로 결정되는 알고리즘 풀이법에는 이렇게 이분탐색법을 활용할수 있으므로 이 개념을 꼭 익혀두면 좋다!

반응형
반응형

알고리즘 문제를 풀다보면 2중배열을 조작하여 풀어야 하는 유형이 종종 보인다.

자주 나오는 유형은 그 풀이공식을 어느정도 외워두는것이 좋은 듯 하다.


문제

크기가 N×M인 배열이 있을 때, 배열을 돌려보려고 한다. 배열은 다음과 같이 반시계 방향으로 돌려야 한다.

A[1][1] ← A[1][2] ← A[1][3] ← A[1][4] ← A[1][5]
   ↓                                       ↑
A[2][1]   A[2][2] ← A[2][3] ← A[2][4]   A[2][5]
   ↓         ↓                   ↑         ↑
A[3][1]   A[3][2] → A[3][3] → A[3][4]   A[3][5]
   ↓                                       ↑
A[4][1] → A[4][2] → A[4][3] → A[4][4] → A[4][5]

예를 들어, 아래와 같은 배열을 2번 회전시키면 다음과 같이 변하게 된다.

1 2 3 4       2 3 4 8       3 4 8 6
5 6 7 8       1 7 7 6       2 7 8 2
9 8 7 6   →   5 6 8 2   →   1 7 6 3
5 4 3 2       9 5 4 3       5 9 5 4
 <시작>         <회전1>        <회전2>

배열과 정수 R이 주어졌을 때, 배열을 R번 회전시킨 결과를 구해보자.

입력

첫째 줄에 배열의 크기 N, M과 수행해야 하는 회전의 수 R이 주어진다.

둘째 줄부터 N개의 줄에 배열 A의 원소 Aij가 주어진다.

출력

입력으로 주어진 배열을 R번 회전시킨 결과를 출력한다.

제한

  • 2 ≤ N, M ≤ 300
  • 1 ≤ R ≤ 1,000 (16926번) / 1 ≤ R ≤ 10**9 (16927번)
  • min(N, M) mod 2 = 0
  • 1 ≤ Aij ≤ 108

 먼저 "반시계 방향" 으로 회전을 어떻게 할 것인가? 를 두고 한참 고민했다. 모두가 같은 방향으로 회전하는 것이 아니라 총 4가지 방향으로 회전해야 하기 때문에
  1. 어느 포인트에서 회전방향을 바꾸어야 하는지
  2. 얼마만큼 길이를 해당 방향으로 회전해야 하는지
를 구하는게 관건이었다.
 
이 문제를 풀기 위해서는 "이중배열을 1차원 배열로 풀기" 가 포인트이다.


1. 이중배열을 1차원 배열로 풀기

1차원 배열로 풀기 위해서는 회전하는 기준이 되는 묶음 (이하 루프) 의 갯수를 파악해야 한다. 제한조건에서 n과 m중 최솟값은 2의 배수임을 명시했기 때문에 루프가 생기지 않는 가운데 부분은 고려하지 않는다.
 

가로, 세로의 최솟값을 2로 나눈 몫 만큼 루프가 형성된다.

# 1. 가로 4, 세로 5
standard = min(4, 5) // 2 # => 2

# 2. 가로 5, 세로 4
standard = min(5, 4) // 2 # => 2

# 3. 가로 2, 세로 4
standard = min(2, 4) // 2 # => 1

# 4. 가로 6, 세로 2
standard = min(6, 2) // 2 # => 1

 

따라서 새로 만들어질 1차원 배열도 루프의 갯수만큼 생성될 것이다.

 

위, 오른쪽, 아래, 왼쪽 순서대로 배열을 새로 정의하는데 이때 "모서리에 있는 숫자" 의 중복 처리를 주의해야 한다.

위와 아래에는 모든 모서리를 포함하도록 하고, 오른쪽과 왼쪽은 모서리를 제외한 변의 숫자만을 처리하도록 한다.

n, m, r = map(int, input().split()) # n = 세로 / m = 가로 / r = 회전 수
board = [list(map(int, input().split())) for _ in range(n)]
new_board = [[0]*m for _ in range(n)]
q = deque()
# 2차원 배열을 1차원 배열로 펼치기
loop_count = min(n, m) // 2

for i in range(loop_count):
    q.clear()
    # 위쪽
    q.extend(board[i][i:m-i])
    # 오른쪽 (모서리 제외)
    q.extend([row[m-i-1] for row in board[i+1:n-i-1]])
    # 아래쪽
    q.extend(board[n-i-1][i:m-i][::-1])
    # 왼쪽 (모서리 제외)
    q.extend([row[i] for row in board[i+1:n-i-1][::-1]])
    q.rotate(-r)

 

또한 위와 아래는 숫자 처리 방향이 반대이고, 오른쪽과 왼쪽도 서로 반대방향이므로 아래쪽과 왼쪽의 수를 배열로 생성할때에는 [::-1] 을 하여 순서를 반대로 되게 해준다.

 

2. r만큼 회전시키기

루프를 1차원 배열로 만든 뒤 r의 횟수만큼 회전을 시켜줌에 따라 pop, popleft, append, appendleft 가 발생한다.

문제는 "반시계 방향" 이므로 popleft와 append가 일어나게 된다.

 

하지만 python의 deque 에서는 회전을 동작하는 rotate 함수가 있으므로 이를 사용해 준다.

왼쪽으로 회전할때는 음의값을, 오른쪽으로 회전할때는 양의 값을 인자로 받는다.

from collections import deque

dq = deque()
dq.extend([1, 2, 3, 4])

# 좌로 회전
dq.rotate(-2) # => [3, 4, 1, 2]

# 우로 회전
dq.rotate(2) # => [3, 4, 1, 2]

# r 만큼 좌로 회전 (반시계 방향)
q.rotate(-r)

 

3. 1차원 배열을 2차원 형태로 가공하기

회전이 완료된 1차원 배열을 다시 원래 자리의 2차원 배열 형태로 만들어 주어야 한다.

별다른 공식은 없고 위치를 주의하여 넣어주면 된다.

    # 위
    for j in range(i, m-i):
        new_board[i][j] = q.popleft()

    # 오른쪽
    for j in range(i+1, n-i-1):
        new_board[j][m-i-1] = q.popleft()
    # 아래쪽
    for j in range(m-i-1, i-1, -1):
        new_board[n-i-1][j] = q.popleft()

    for j in range(n-i-2, i, -1):  # 왼쪽
        new_board[j][i] = q.popleft()

q 에는 회전이 완료된 수들이 순서대로 들어있다. 1차원 배열에 담았던 순서대로 위 -> 오른쪽 -> 아래쪽 -> 왼쪽 으로 처리해 준다.

이때 for-loop 에서 range(a, b, c) 형태는 a 부터 b 까지 c의 값만큼 더하여 처리한다는 뜻이다.

 


전체 풀이

import sys
from collections import deque
sys.stdin = open("input.txt", "r")
n, m, r = map(int, input().split())
board = [list(map(int, input().split())) for _ in range(n)]
new_board = [[0]*m for _ in range(n)]
q = deque()
# 2차원 배열을 1차원 배열로 펼치기
loop_count = min(n, m) // 2

for i in range(loop_count):
    q.clear()
    # 위쪽
    q.extend(board[i][i:m-i])
    # 오른쪽 (모서리 제외)
    q.extend([row[m-i-1] for row in board[i+1:n-i-1]])
    # 아래쪽
    q.extend(board[n-i-1][i:m-i][::-1])
    # 왼쪽 (모서리 제외)
    q.extend([row[i] for row in board[i+1:n-i-1][::-1]])
    q.rotate(-r)

    # 다시 board 정렬하기
    # 위
    for j in range(i, m-i):
        new_board[i][j] = q.popleft()

    # 오른쪽
    for j in range(i+1, n-i-1):
        new_board[j][m-i-1] = q.popleft()
    # 아래쪽
    for j in range(m-i-1, i-1, -1):
        new_board[n-i-1][j] = q.popleft()

    for j in range(n-i-2, i, -1):  # 왼쪽
        new_board[j][i] = q.popleft()

for row in new_board:
    print(*row)

"""
[입력]
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
"""

"""
[출력]
3 4 8 12
2 11 10 16
1 7 6 15
5 9 13 14
"""

 

16926번과 16927번의 문제가 동일하여 같은 풀이로 제출하면 되는데

앞의것은 실버레벨이고 뒤의것은 골드레벨인것을 보아, 16926번을 풀때 deque를 활용하지 않고 순수 배열 처리로만 했을때 풀리더라도 같은 풀이로 16927번을 풀었을때 타임아웃이 나지 않을까 싶다.

제한 조건을 보면 16926은 R의 값이 최대 1000 이지만 16927은 최대 10**9 값을 가질수 있다.

 

위 풀이로는 두 문제 전부 python3으로 체점하여 통과 가능하다.

반응형
반응형

우리회사에서는 유저의 행동기록을 남기고 있다. 상품에 대한 처리, 주문에 대한 처리, 유저가 활동한 기록들 등..

이런 기록들이 서비스 오픈 이후로 별다른 정리조치 없이 꾸준히 쌓이고 있는데,

아무래도 서비스 운영기간이 늘어날수록 db용량이 늘어나는건 당연한 수순이지만, aws 비용 줄이기를 하고 있는 요즘

mongodb의 ec2 용량마저 줄이기 위해서 데이터 정리를 하게 되었다.

 

1. 현재 가용중인 mongodb의 ec2 용량 확인하기.

회사 mongodb는 aws의 ec2에 서버를 띄워 사용하고 있다. 그리고 EBS로 EC2의 용량을 지정해 두었다.

amazonEBS는 AWS 클라우드의 EC2 인스턴스에 사용할 영구 블록 스토리지 볼륨을 제공한다.
각 EBS 볼륨은 가용 영역 내에 자동으로 복제되어 구성요소 장애로부터 보호해주고 고가용성 및 내구성을 제공한다.

 

현재 설정된 mongodb EC2의 EBS 볼륨은 310GiB 이다.

왜 primary랑 secondary가 다르냐면... 바로 얼마전에 300GiB 에서 DB에 OOM이 발생하였기 때문이다 ㅎ

200GiB에서 올린지 1년도 채 되지 않았는데 그 사이에 유저들의 활동량이 폭발적으로 늘어나면서 금방 용량이 꽉 찬듯 하다.

이때 급해서 primary만 먼저 10GiB 정도 올리고 동시에 불필요한 히스토리 데이터를 삭제작업 하면서 최종적으로 가용중인 용량은 310GiB중 267GiB 이다.

 

+) ec2 mongodb 용량 확인하기 : df -h

[ec2-user@ip-**** ~]$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p1  310G  267G   44G  87% /

 

미리 체크하면서 데이터 정리 했으면 안늘려도 됐겠군....ㅎ 그치만 데이터 정리에 대한 정책이 아예 없었었다!!!!!!!!!!!!!

 

이후 데이터 정리에 대한 여러번의.. 지시가 내려오면서... 다양한 기준으로 데이터 정리를 하기 시작했다.

- 장기 미접속 유저의 데이터 삭제...

- 탈퇴유저의 즉시 데이터 삭제..

그리고 이번에 작업한 "1년지난 히스토리 데이터 삭제" 이다.

 

2. 데이터 삭제하기

+) index 걸기

데이터 삭제를 위해서 기준값을 갖고 delete를 해주는데, 이때 잡은 기준값 key에 index가 몇개 collection엔 없었다.

일단 해~ 하면서 삭제프로세스를 시작했고 예상되는 소요시간 120시간 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

 

바로 중단 후 index 작업을 해주었다.

그리고 나서 걸린 시간 2시간! 새로운 collection를 구상할땐 index 꼭좀 걸어주세용... 뒤늦게 걸려니 영...


 

데이터 삭제 후 다시 가용용량을 확인해 봤는데 용량이 전혀 줄지 않았다! 띠용..

이는 "mongodb에서 삭제된 데이터에 대한 디스크 용량을 즉시 반환하지 않기 때문" 이다.

데이터를 삭제한 이유가 용량확보를 위한 것이므로 즉시 용량을 확보해주면 된다.

 

3. 용량 확보하기 : compact

 

+ collection lock 확인

mongodb에서 용량을 확보하는 명령어는 compact 이다.

해당 명령어 사용시 collection에 lock은 걸리지 않지만 가장 안전한 방법으로는

- 현재 사용중인 db와 를 복제..

- 데이터 마이그레이션 ..

이렇게 하는거라고 하는데 몇천만개의 데이터를 마이그레이션 하려면 거진 하루정도는 서비스를 내려야 하지 않나 싶다 ㅎ

아니면 내리지 않고 ... 어.. 음.. 마이그레이션 하는동안 생성된 데이터를 임시보관 해뒀다가 마이그레이션 한 뒤 다시 insert 해주는...? 어우.. 이게 맞나..? 무튼 복잡하다!


당장의 용량확보가 최우선순위였고, 삭제된 데이터는 운영하는데 아무런 지장이 없는 데이터이므로 운영환경에서 compact 해주기로 한다.

 

먼저 ec2의 mongodb 로 접속한다. 그리고 다시한번 용량 확인

[ec2-user@ip-*** ~]$ ssh 운영환경 ec2
       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|
[ec2-user@ip-*** ~]$ ssh mongo
Last login: ~~~

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|
[ec2-user@ip-*** ~]$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p1  310G  267G   44G  87% /

 

이후 mongodb shell 을 열어준다.

[ec2-user@ip-*** ~]$ mongo
MongoDB shell version v5.0.13
connecting to: mongodb://127.0.0.1:27017/? ~~~~~~
================
... 안내문 생략 ...
================
replica:PRIMARY>

db가 replica set 설정이 되어있어 primary 로 연결되었다.

 

+) compact 명령어 실행 권한 에러

compact 명령어는 db의 관리자가 수행할수있는 명령어이다. 관리자 권한이 아닌 상태에서 명령어 실행시 에러가 발생한다.

replica:PRIMARY> db.runCommand( {compact : '{target Collection}', force : true} )
{
	"ok" : 0,
	"errmsg" : "command compact requires authentication",
	"code" : 13,
	"codeName" : "Unauthorized",
	"$clusterTime" : {
		"clusterTime" : Timestamp(~~~, ~~),
		"signature" : {
			"hash" : BinData(0,"~~~"),
			"keyId" : NumberLong("~~")
		}
	},
	"operationTime" : Timestamp(~~~, ~~)
}

 

때문에 대상 db로 들어가 관리자로 접속후 진행해야 한다.

현재 관리자 user 생성된게 없어서 먼저 생성부터 해준다. shell 에서 생성해주려나 authentication error 가 발생하는데 이부분은 어떻게 생성하는지 방법을 몰라서 mongodb tool 에서 해당 db의 users 항목에서 add 해주었다.

db.createUser(
   {
     user: "dbAdmin", pwd: "password", roles: [{"role":"dbAdmin","db":"targetDB"}]
   }
)

 

이후 다시 mongo shell 에서 어드민 권한을 사용해 compact를 수행하준다.

replica:PRIMARY> use {작업할 DB명 : 이하 targetDB}
switched to db targetDB
replica:PRIMARY> db.auth('dbAdmin123123', 'password')
Error: Authentication failed.
0  # 접속 실패
replica:PRIMARY> db.auth('dbAdmin', 'password')
1  # 접속 성공

1 이 나오면 관리자 권한으로 연결됐단 뜻이다.

 

이후 compact 명령어를 실행해준다. 시간이 오래 걸릴 경우 shell 이 멈춘것처럼 보이는데 이때 다른 터미널을 열어서 mongo 용량을 확인해보면 실시간으로 용량이 줄어드는걸 볼 수 있다 ㅎㅎ

replica:PRIMARY> db.runCommand( {compact : '{target Collection}', force : true} )
{
	"bytesFreed" : ******,
	"ok" : 1,
	"$clusterTime" : {
		"clusterTime" : Timestamp(***, *),
		"signature" : {
			"hash" : BinData(0,"******"),
			"keyId" : NumberLong("******")
		}
	},
	"operationTime" : Timestamp(***, *)
}
# 작업 후 db 용량
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p1  310G  266G   45G  86% /

 1 기가 줄었군......................

 

이후로도 계속 작업을 진행해준다. 보다 상세한 비교를 위해 compact 수행 전 collection의 stats를 확인해보고 명령어 실행 이후와 함께 비교해 보았다.

확인 할 부분은 "storageSize" 이다. 데이터를 삭제하기 전과 용량이 동일하게 잡혀있다.

compact 명령어 실행 이후는 아래와 같다.

7.5GiB 로 용량이 줄어들었다!

전체 db 용량도 확인해보니 259GiB로 확인된다.

[ec2-user@ip-*** ~]$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p1  310G  259G   52G  84% /

 

임무 완료!

반응형
반응형

입력값 처리

python에서 sys 가 있다면 java에서는 import java.util.Scanner 가 있다.
이때 Scanncer는 InputStream을 생성자로 입력받는데, 입력하는 곳이 어디냐에 따라 그 값을 넣어주면 된다.

[1] 입력 방식의 차이

1-1) 키보드에서 입력받기 : System.in

키보드에서의 입력을 받으려면 System.in의 InputStream 을 넘겨주면 된다.

import java.util.Scanner

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
    }
}

1-2) 변수에서 입력받기 : 변수명

키보드에서의 입력을 받으려면 System.in의 InputStream 을 넘겨주면 된다.

import java.util.Scanner

public class Main {
    public static void main(String[] args) {
        String ex = "2 apple";
        Scanner sc = new Scanner(ex);
    }
}

1-3) 외부 파일에서 입력받기 : new File()

import java.util.Scanner

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(new File({파일명}));
    }
}

2) 타입에 따라 입력받기

파이썬 처럼 입력받을때 타입을 지정할 수 있다.

import java.util.Scanner

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        // int 형으로 입력받기 : Short(), Long(), Double(), Float() 도 가능하다.
        int input_int = sc.nextInt();

        // 문자로 입력받기
        String input_str = sc.next();
        // 문자를 '줄' 단위로 입력받기
        String input_str = sc.nextLine();
    }
}

이때 next()nextInt() 는 모두 기본적으로 공백을 구분자 로 하여 한개 단어씩만 입력받는다.


public class Main {
    public static void main(String[] args) {
        String ex = "2 apple";
        Scanner sc = new Scanner(ex);

        int input_int = sc.nextInt(); // 2   
        String input_str = sc.next();  // apple

        System.out.println(input_int+input_srt);
    }
}
>>> 2apple

3) 구분자로 입력받기

3-1) 한줄로 입력

한줄에 구분자를 두고 여러값을 입력받는 경우도 있다.

3
1 6 7

이런식의 입력예제는 알고리즘 문제에서 많이 볼수 있었다. nextLine()을 통해 개행문자로 구분되기 전 한줄의 값을 모두 읽어올 수 있다.

public class Main {
    public static void main(String[] args) {
        String ex = "2 apple";
        Scanner sc = new Scanner(ex);

        String input_line = sc.nextLine();        
        System.out.println(input_line);
    }
}
>>> 2 apple

3-2) 입력받은 문자열을 공백을 기준으로 리스트로 담기

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("공백으로 구분된 문자열을 입력하세요:");
        String input = scanner.nextLine();

        // 입력된 문자열을 공백으로 분리하여 배열로 저장
        String[] tokens = input.split("\\s+");
    }
}

이때 배열상태인 tokens를 바로 출력하면 배열이 저장된 데이터의 주소값 이 출력된다. 이를 문자로 출력하기 위해선 Arrays.toString() 메소드를 이용해 배열을 문자열로 변환한 후에 출력해야 한다.

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("공백으로 구분된 문자열을 입력하세요:");
        String input = scanner.nextLine();

        // 입력된 문자열을 공백으로 분리하여 배열로 저장
        String[] tokens = input.split("\\s+");

        // 배열을 리스트로 변환
        List<String> list = Arrays.asList(tokens);

        // 리스트 출력 (테스트용)
        System.out.println("리스트: " + list);
    }
}

3-3) 입력받은 한줄을 숫자형 리스트로 만들기

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("공백으로 구분된 숫자를 입력하세요:");
        List<Integer> numbers = new ArrayList<>();

        while (scanner.hasNextInt()) {
            numbers.add(scanner.nextInt());
        }

        // 숫자 리스트 출력
        System.out.println("입력된 숫자: " + numbers);
    }
}

이때 nextLine() 처럼 한줄을 한번에 읽고, 그 후에 처리하는 방법은 아래와 같다.

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("공백으로 구분된 숫자를 한 줄에 입력하세요:");
        String input = scanner.nextLine();

        // 입력된 문자열을 공백으로 분리하여 숫자로 변환하여 리스트에 저장
        List<Integer> numbers = new ArrayList<>();
        String[] tokens = input.split("\\s+");
        for (String token : tokens) {
            numbers.add(Integer.parseInt(token));
        }

        // 숫자 리스트 출력
        System.out.println("입력된 숫자: " + numbers);
    }
}
반응형

+ Recent posts