백준 1938번 통나무 옮기기

#1938. 통나무 옮기기

Problem

Solution

  1. 통나무 중점 좌표를 토대로 BFS 탐색을 하였다.
    중심을 기준으로 그대로(회전), 상, 하, 좌, 우로 이동하면 각각 작동횟수 + 1이기에
    BFS로 탐색하는게 최적이다.
    중복 탐색을 막기 위해 3차원 방문 배열을 두어 중점 좌표의 각 모양(타입)에 따라 표시를 하였다.
  2. 중점 좌표로 이동하다보니 이동 후 다음을 꼭 확인해야 한다.
    1. 평지 범위를 벗어나지 않는지
      세 좌표 모두 범위를 벗어나지 않도록 확인해야한다.
    2. 방문한 지점인지
    3. 움직일 수 있는지
      상하좌우 → 움직인 세 좌표에 ‘1’이 없는지 확인
      회전 → 움직인 중점좌표 기준으로 3x3dp ‘1’이 없는지 확인
  3. 도착 지점에 도착하면 종료

1 Try

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <iostream>
#include <vector>
#include <queue>
#define endl "\n"
#define MAX 50
using namespace std;
int N, ans = 1e9;
char land[MAX + 1][MAX + 1];
bool visited[MAX + 1][MAX + 1][2];
vector<pair<int, int>> start_point;
vector<pair<int, int>> end_point;
int dr[5] = { 0, -1, 1, 0, 0 };
int dc[5] = { 0, 0, 0, -1, 1 };
struct Log {
int type; // 0 : 가로, 1 : 세로
int r, c; // 중점 좌표
};
bool isIn(int r, int c, int type) { // 통나무가 평지 범위인지
if (type == 0) c--;
else r--;
for (int i = 0; i < 3; ++i) {
if (r < 0 || c < 0 || r > N - 1 || c > N - 1) return false;
if (type == 0) c++;
else r++;
}
return true;
}
bool isEnd(int r, int c, int type) { // EEE에 도착했는지
if (type == 0) c--;
else r--;
for (int i = 0; i < end_point.size(); i++)
{
if (end_point[i].first != r || end_point[i].second != c) return false;
if (type == 0) c++;
else r++;
}
return true;
}
bool Check(int r, int c, int type) { // 이동이 가능한지
if (type == 0) c--;
else r--;
for (int i = 0; i < 3; ++i) {
if (land[r][c] == '1') return false;
if (type == 0) c++;
else r++;
}
return true;
}
bool CheckRotate(int r, int c, int type) { // 회전이 가능한지
int sr = r - 1, sc = c - 1;
for (int i = sr; i < sr + 3; ++i) {
for (int j = sc; j < sc+ 3; ++j) {
if (land[i][j] == '1') return false;
}
}
return true;
}
void BFS() {
queue<Log> q;
int type, r, c;
if (start_point[0].first == start_point[1].first) {
type = 0; r = start_point[0].first; c = start_point[1].second;
}
else {
type = 1; c = start_point[0].second; r = start_point[1].first;
}
q.push({ type, r, c });
visited[r][c][type] = true;
int cnt = 0;
while (int s = q.size()) {
while (s--) {
int r = q.front().r, c = q.front().c;
int type = q.front().type;
if (isEnd(r, c, type)) {
ans = cnt;
return;
}
q.pop();
for (int dir = 0; dir < 5; ++dir) {
if (dir == 0 || dir == 1) type = (type+1) % 2; // 회전 후 타입 원래대로
int nr = r + dr[dir];
int nc = c + dc[dir];
if (!isIn(nr, nc, type)) continue;
if (visited[nr][nc][type]) continue;
if (dir == 0) if (!CheckRotate(nr, nc, type)) continue; // 3 x 3 확인 후 회전
if (Check(nr, nc, type)) {
visited[nr][nc][type] = true;
q.push({ type, nr, nc });
}
}
}
cnt++;
}
}

int main() {
cin >> N;
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
cin >> land[i][j];
if (land[i][j] == 'B') start_point.push_back({ i, j });
else if (land[i][j] == 'E') end_point.push_back({ i, j });
}
}
BFS();
if (ans == 1e9) ans = 0;
cout << ans < endl;
return 0;
}

백준 1932번 정수 삼각형

#1932. 정수 삼각형

문제링크

Problem

  • 맨 위층부터 시작 → 맨 아래 층
    선택된 수들을 합하면서 내려옴
  • 현재 층에서 선택된 수의 대각선(왼 or 오)만 가능
  • condition
    • 층은 최대 500
    • 수의 범위 0~9999
  • Goal : 합이 최대가 되는 수

Solution

  • 입력을 보면 알겠지만 자기 자신 바로 아래와 오른쪽만 가능
    1
    2
    3
    4
    5
    6
    // input
    7
    3 8
    8 1 0
    2 7 4 4
    4 5 2 6 5
  • 그냥 재귀함수를 쓰면 반복되는 호출이 많이 일어난다.
    DP를 사용해야 함을 알 수 있다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // #0
    7
    // #1
    7+3 / 7+8
    // #2
    7+3+8, 7+3+1 / 7+8+1, 7+8+0
    // #3
    7+3+8+2, 7+3+8+7 / 7+3+1+7, 7+3+1+4 / 7+8+1+7, 7+8+1+4 / 7+8+0+4, 7+8+0 +4
    // #4
    20+4, 20+5 / 25+5, 25+2 / 18+5, 18+2 / 14+2, 14+6 / 23+5, 23+2 / ...
    경우의 수는 1→2→4→8→16으로 늘어난다.

500일 때 최대 500^2 = 250000(25만)의 경우의 수가 나온다. 물론 재귀함수를 사용하면 이보다 더 많은 함수 호출이 일어나 시간초과가 발생할 것이다.

6개월 전에 풀었던 것을 다시 풀어보려니…생각이 안난다.

dp[a][b]

1 Try (6개월 전 코드)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <algorithm>
using namespace std;
int t[500][500];
int d[500][500];
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i++) {
for (int j = 0; j <= i; j++)
{
cin >> t[i][j];
}
}
d[0][0] = t[0][0];
for (int k = 1; k < n; k++)
{
for (int h = 0; h < n; h++)
{
if (h == 0)
{
d[k][0] = d[k - 1][0] + t[k][0]; //바로 위의 최대경로만 가져올 수 있음.
}
else if (k == h)
{
d[k][h] = d[k - 1][h - 1] + t[k][h]; // 바로 위 왼쪽의 최대경로만 가져올 수 있음.
}
else {
d[k][h] = max(d[k - 1][h - 1], d[k - 1][h]) + t[k][h];
}
}
}
int max_cost = 0; // 0보다 작을 수 없음.
for (int index = 0; index < n; index++)
{
max_cost = max(d[n - 1][index], max_cost);
}
cout << max_cost << endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <algorithm>
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
int max_sum = 0;
vector<vector<int>> dp;
int main() {
int n, input;
scanf(" %d", &n);
dp.resize(n);
vector<vector<int>> tri(n);
for(int i = 0; i < n; ++i) {
for(int j = i; j >= 0; --j) {
scanf(" %d", &input);
tri[i].push_back(input);
}
}
dp[0][0] = tri[0][0];
for(int i = 1; i < n; ++i) {
for(int j = 0; j <= i; ++j) {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-1]);
}
}

cout << max_sum << endl;
return 0;
}

백준 1525번 퍼즐

1525. 퍼즐

Problem

Solution

  • 상당히 어려운 문제다. (접근법을 알아둘 필요가 있다.)
  • 접근법
  1. 2차원 배열을 1차원 배열로 생각하기
  2. 퍼즐에 적혀있는 숫자를 하나로 쭉 이어진 수로 생각한다.
  3. 이어진 수 하나가 경우의 수라고 생각한다. (문제 목표는 123456789인 수(경우)를 찾는 것)
  4. map<해당 경우(수), 이동 횟수>를 사용하여 해당 경우에 도달하기까지 걸리는 이동 횟수를 저장한다.
  5. 9(0)이 있는 위치에서 시작하여 BFS 탐색을 하고 탐색 시에 swap을 해야 한다. (이동을 할 때 인덱스 계산에 주의한다.)
  6. swap을 위해 string을 사용한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ex) 현재 193425786 (0대신 9를 해야 각 자릿 수가 모두 채워진다. 0123...으로 하면 0이 사라짐)
    193425786
    -> 913425786 (왼쪽 이동)
    -> 123495786 (아래쪽 이동)
    -> 149425786 (오른쪽 이동)

    3 x 3
    0 1 2
    3 4 5
    6 7 8
    행 = 9번 위치(0~8 중) / 3
    열 = 9번 위치 % 3
  • 주의

아래 코드에서 dist.count(next_num) == 0 대신 dist[next_num] == 0 을 하면 틀리다.
dist[해당 수]에는 이동 횟수가 들어있고 dist.count(해당 수)는 해당 경우의 수가 몇 번 나왔는지 알려주기 때문이다. map에서 해당 키, 값을 넣어주지 않았는데 바로 해당 키에 대한 값을 참조하려고(dist[next_num] == 0) 하면 제대로 연산이 수행되지 않을 것이다.

1 Try

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <queue>
#include <map>
#include <string>
using namespace std;
int dx[4] = { -1, 1, 0, 0 };
int dy[4] = { 0, 0, -1, 1 };
queue<int> q;
map<int, int> dist;
void BFS(int start) {
q.push(start);
dist[start] = 0;
while (!q.empty()) {
int now_num = q.front();
q.pop();
string now = to_string(now_num);
int zero = now.find('9'); // 0의 위치
int x = zero / 3; // 행
int y = zero % 3; // 열
for (int dir = 0; dir < 4; ++dir) {
int d_x = x + dx[dir];
int d_y = y + dy[dir];
if (d_x > -1 && d_y > -1 && d_x < 3 && d_y < 3) {
string next = now;
swap(next[x * 3 + y], next[d_x * 3 + d_y]); // 문자열 인덱스(2차원->1차원)
int next_num = stoi(next);
if (dist.count(next_num) == 0) {
q.push(next_num);
dist[next_num] = dist[now_num] + 1;
}
}
}
}
}
int main() {
string s = "";
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
int num; cin >> num;
if (num == 0) num = 9;
s += to_string(num);
}
}
int start = stoi(s);
BFS(start);
if (dist.count(123456789) == 0) cout << -1 << "\n";
else cout << dist[123456789] << "\n";
return 0;
}

백준 1261번 알고스팟

1261. 알고스팟

Problem

Solution

  • 벽을 최소한으로 부수면서 목적지에 도착해야 한다.
  • 벽을 부수지 않고 갈 경우 비용은 0
  • 벽을 부수고 갈 경우 비용은 1
  • 따라서 deque를 사용하여 BFS 탐색을 한다.
  1. 벽을 부수지 않는 경우 front에 넣는다.
  2. 벽을 부수는 경우 back에 넣는다.
  3. front 부분을 탐색하고 pop한다.
    그래야 벽을 최소한으로 부수면서 visit(방문) 표시가 가능하다.

1 Try

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <cstdio>
#include <tuple>
#include <deque>
using namespace std;
int N, M;
int map[100][100];
bool visit[100][100];
int cnt[100][100];
int dx[4] = { -1, 1, 0, 0 };
int dy[4] = { 0, 0, -1 ,1 };
void Input() {
scanf("%d %d", &N, &M);
for (int i = 0; i < M; ++i) {
for (int j = 0; j < N; ++j) {
scanf("%1d", &map[i][j]);
}
}
}
void BFS() {
deque <pair<int, int>> q;
q.push_front({ 0, 0 });
visit[0][0] = true;
while (!q.empty()) {
int x, y;
tie(x, y) = q.front();
q.pop_front();
for (int dir = 0; dir < 4; ++dir) {
int d_x = x + dx[dir];
int d_y = y + dy[dir];
if (d_x == N - 1 && d_y == M - 1) {
cnt[d_x][d_y] = cnt[x][y];
return;
}
if (d_x > -1 && d_y > -1 && d_x < M && d_y < N) {
if (visit[d_x][d_y]) continue;
if (map[d_x][d_y] == 1) {
cnt[d_x][d_y] = cnt[x][y] + 1;
q.push_back({ d_x, d_y });
}
else {
cnt[d_x][d_y] = cnt[x][y];
q.push_front({ d_x, d_y });
}
visit[d_x][d_y] = true;
}
}
}
}
int main() {
Input();
BFS();
printf("%d\n", cnt[M - 1][N - 1]);
return 0;
}

Jenkins, NAVER Cloud Platform, Docker로 CI/CD 무중단 배포 환경 구축하기 - 2편

2편에서는 CD에서 무중단 배포까지 구축한다.

1편 보러가기

(1편에서는 Github Push 후에 Jenkins가 자동으로 빌드되도록 설정하였다.)

2. Jenkins로 Node.js기반 프로젝트 배포 설정하기

배포를 위해 scp를 사용하여 Jenkins에 있는 프로젝트 코드를 NAVER Cloud 서버에 복사해야 한다.
Node.js 기반의 프로젝트는 Tomcat과 같은 WAS(웹 서버+웹 컨테이너)가 존재하지 않아 ssh로 접속, scp로 파일을 주고 받는 작업으로 비교적 간단히 배포할 수 있다.

2-1. NAVER Cloud 서버에도 ssh 키를 생성한다.

1
ssh-keygen -t rsa

키가 생성되면 authorized_keys, id_rsa, id_rsa.pub, known_hosts 가 생성되어 있는 것을 볼 수 있다.

scp로 파일을 주고 받는다 하였는데 이때 복사 받을 서버의 접속 비밀번호를 알아야 한다. 그렇게되면 복사할 때마다 매번 비밀번호를 요구하여 자동화 배포는 불가능하게 된다.

이를 해결하기 위해 위의 4개의 키를 이용한다.
Jenkins에서는 NAVER Cloud 서버를 호스트로, NAVER Cloud 서버에서는 Jenkins 서버의 공개 키를 허가받은 키로 등록한다. 이러면 비밀번호를 요구하지 않고 자동화 배포가 가능하다.

2-2. Jenkins 서버에서 NAVER Cloud 서버를 known_hosts로 등록한다.

1
ssh-keyscan -H (NAVER CLOUD 서버)ip >> ~/.ssh/known_hosts

2-3. Jenkins에서 생성한 키 중에 공개 키(id_rsa.pub)를 NAVER Cloud 서버에서 생성한 authorized_keys에 입력한다.

  • Jenkins 서버의 공개 키

  • NAVER Cloud 서버의 authorized_keys

    1
    vi ~/.ssh/authorized_keys

  • scp로 테스트하기
    접속 비밀번호를 요구하지 않았다면 성공이다.

  1. Jenkins 서버에서 test.txt 파일을 생성한다.
  2. Jenkins 서버에서 scp를 사용해 test.txt를 NAVER Cloud 서버의 /root/test 폴더에 복사한다.
    비밀번호를 요구하지 않으면 성공.
  3. NAVER Cloud 서버에서 복사된 파일을 확인한다.

3. NAVER Cloud 서버에 Docker, NGINX 설정하기

이제 프로젝트가 배포되는 NAVER Cloud 서버에서 설정해야 할 부분이다.
(*Docker 및 NGINX 설치는 생략한다.)

3-1. Dockerfile 작성.

ssh로 서버에 접속하고 Dockerfile을 다음과 같이 작성한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM 이미지 환경

MAINTAINER 개발자

VOLUME 호스트와 공유할 폴더

RUN mkdir -p /app

WORKDIR /app

COPY ./프로젝트 소스코드 폴더/ /app

RUN npm install

CMD npm start

  • FROM: Node 이미지(Node version 명시)
  • MAINTAINER: 개발자(팀명)
  • VOLUME: host와 컨테이너가 공유할 폴더 경로
  • RUN: 해당 명령어 실행( 위에선 app 폴더 생성 )
  • WORKDIR: 작업 디렉토리 설정(위에서 만든 app 폴더로 설정)
  • COPY: NAVER Cloud 서버에 있는 파일을 컨테이너 폴더에 복사
    서버에 있는 파일은 Dockerfile이 존재하는 현재 경로 내에 존재하는 것만 가능하다.
    (즉, 절대 경로는 Dockerfile이 있는 경로 이내가 아니라면 불가능)

그 다음은 패키지 파일들을 설치하고 서버를 실행한다.

docker image build -t 도커이미지이름 .를 입력하여 이미지를 빌드한다.

3-2. Docker-compose 작성하기

docker-compose(컨테이너 관리)를 설치하여 편하게 컨테이너를 설정한다.
여기서 blue-green 배포 방식이 사용되는데, 이는 무중단 배포 구축을 위함이다.

  • Dockerfile이 있는 곳에 docker-compose.blue.yml을 다음과 같이 작성한다.


1
2
3
4
5
6
7
8
9
version: '2'

services:
linking-server:
image: 빌드한 도커 이미지
volumes:
- ./deploy:/deploy/linking
ports:
- "NAVER Cloud 서버 포트:컨테이너 포트"

image: 아까 빌드한 이미지 이름을 작성한다.

volumes: host 폴더와 컨테이너 폴더를 설정하여 공유한다.(심볼릭 링크와 비슷한 개념이라고 한다.)

ports: host 포트와 컨테이너 포트 순으로 작성하여 포트 포워딩 설정을 한다.

  • 같은 곳에 docker-compose.green.yml을 다음과 같이 작성한다.
    host 포트만 다르다.

3-3. deploy script 작성하기.

  • 같은 곳에 deploy.sh를 다음과 같이 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

DOCKER_APP_NAME=linking-server

EXIST_BLUE=$(docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml ps | grep Up)

if [ -z "$EXIST_BLUE" ]; then
echo "blue up"
docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml up -d

sleep 10

docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml down
else
echo "green up"
docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml up -d

sleep 10

docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml down
fi
  • 지금까지 잘 따라 했다면 아래와 비슷한 디렉토리 구조를 볼 수 있을 것이다.

  • 컨테이너 생성하기

docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml up -d를 입력하여 blue 컨테이너를 먼저 생성해준다.

docker ps -a로 컨테이너가 잘 실행되고 있는지 확인할 수 있다.

3-4. NGINX 설정하기.

다음은 NGINX로 blue, green의 로드밸런싱을 설정해 주어야 한다.

  • vi /etc/nginx/sites-available/linking-server를 입력하여 다음과 같이 작성한다.
    linking-server는 필자가 정한 이름이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
# Load Balancing
upstream linking-server {
least_conn;
server 127.0.0.1:1025 weight=5 max_fails=3 fail_timeout=10s;
server 127.0.0.1:1026 weight=10 max_fails=3 fail_timeout=10s;
}
server {
listen 1024;
server_name NAVER Cloud 서버 아이피;
location / {
proxy_pass http://linking-server;
}
}

그런 다음 아래 명령어로 이 파일을 /etc/nginx/sites-enabled 디렉터리에 링크해준다.

sudo ln -fs /etc/nginx/sites-available/linking-server /etc/nginx/sites-enabled/

마지막으로 sudo nginx -t 명령어로 문법 이상 유무를 확인하고 이상이 없을 경우, systemctl stop nginx 명령어로 NGINX를 종료한 후에 systemctl start nginx 로 다시 시작한다.

3-5. NAVER Cloud Platform ACG 설정하기.

여기서 마지막으로 한 가지 해야할 것은 ACG 설정이다.

  • ACG 설정하기

NAVER Cloud Platform 콘솔에서 1024포트를 열어준다. 위에서 blue, green 모두 다른 포트지만 NGINX를 통해 1024 포트로 로드 밸런싱 되기에 사용자는 이를 통해 접근할 수 있다.
(보통 백엔드와 프론트엔드를 분리하면서 프로젝트를 진행하고 있다면 프론트엔드는 80포트를 사용하는게 좋다. 80이 기본 포트라 생략 가능하므로)

이것으로 NAVER Cloud 서버에서의 설정은 끝났다.

4. 마지막 작업

이제 마지막 Jenkins에서 빌드할 때 설정을 해주어야 위에서 설정한 무중단 배포가 자동화된다.

  • Jenkins 프로젝트 관리에서 “Execute managed script”를 클릭한다.
    다만, 아직 작성한 스크립트가 없기에 Jenkins 관리 > Managed files > Add a new Config를 통해 스크립트를 작성한다.

  • 다음과 같이 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/sh
ssh -T root@서버 아이피 <<EOF
rm -rf /home/docker-image/deploy/*
exit
EOF

scp -r /var/lib/jenkins/jobs/LinkingCI/workspace/* root@서버 아이피:/home/docker-image/deploy/

#!/bin/sh
ssh -T root@서버 아이피 <<EOF
cd /home/docker-image
docker image build -t linking-server-docker-image .
./deploy.sh
exit
EOF

1) Jenkins가 빌드 동안 해당 서버에 접속하여 원래 있던 애플리케이션 코드를 삭제한다.

2) 빌드된 파일을 해당 서버에 복사한다.

3) 해당 서버에 다시 접속하여 새로 복사된 파일을 토대로 도커 이미지를 빌드한다.

4) 배포 스크립트를 실행한다.

위와 같은 과정이 일어나고 지금까지 설정한 것들이 모두 자동화되어 무중단 배포까지 진행된다. 이를 이제 Jenkins 프로젝트 관리에서 실행하도록 한다.

5. 빌드 상태 표시, Slack 알림

추가로 프로젝트에서 보이는 Embeddable Build Status를 클릭하면 마크다운 형식으로 Github 레포지토리에 빌드 상태바를 보이게 할 수 있다.

마지막으로 팀원들이 빌드 상태(시작, 실패, 성공)을 알고 싶을 때 Slack을 통해 알림을 받도록 할 수도 있다.
1편에서 이미 관련 플러그인을 설치하였기에 바로 설정이 가능하다.

  • Slack에 채널을 만들었다면 Apps에서 Jenkins를 검색 후 클릭한다.

  • 알림을 받을 채널을 설정하면 Jenkins에서 어떻게 설정해야 하는지 친절하게 나온다.

설정을 하고 나면 Jenkins 프로젝트에서 빌드 후 조치에 알림 받을 내역을 설정할 수가 있게 된다.

  • 빌드 시작부터 끝까지 알림이 날라온다.

이것으로 CI/CD 무중단 배포 환경 구축을 마친다.

-끝-


Java는 call by value 일까?

Java에서 모든 것은 pass-by-value입니다. 참조가 아닌 값에 의해 호출됩니다.

stackoverflow에서 찾은 예제로 설명드리겠습니다.

예제1.

  • myCat은 Cat이 아닙니다. Cat에 대한 포인터입니다. 즉, 주소값을 갖고 있습니다.
    1
    2
    Cat myCat = new Cat("Rover");
    foo(myCat);
    따라서 만들어진 Cat 객체의 주소가 foo 메소드에 넘겨지게 됩니다.

Cat 객체의 주소를 42라고 가정하겠습니다. 42가 메소드에 넘겨지겠죠?
foo 메소드가 다음과 같다면,

1
2
3
4
5
public void foo(Cat someCat) {
someCat.setName("Max"); // 1.
someCat = new Cat("Fifi"); // 2.
someCat.setName("Rowlf"); // 3.
}

someCat 은 주소값 42를 갖게 됩니다.

  • line 1.
    • someCat은 42라는 주소에 해당하는 Cat을 가리킵니다.
    • Max로 이름을 바꿉니다. (someCat의 이름은 Max가 됩니다.)
  • line 2.
    • 이름이 Fifi인 새로운 Cat 객체가 생성됩니다.
    • 주소는 74라고 가정하겠습니다.
  • line 3.
    • someCat은 74라는 주소에 해당하는 Cat을 가리킵니다.
    • Rowlf로 이름을 바꿉니다. (someCat의 이름은 Rowlf가 됩니다.)

이제, 메소드가 끝났습니다. myCat은 변경되었을까요?

정답은 “변경되지 않았다.” 입니다.
myCat은 여전히 주소값 42를 갖고 있습니다. 하지만 주목해야 할 점은 “이름은 Max로 변경되었다.” 라는 것입니다.

C 언어를 공부하셨던 분들이라면 C와 유사하지만 다르다 라는 것을 느끼셨을 겁니다. C와 마찬가지로 주소값이 전달되지만, C와 다르게 포인터가 가리키는 곳을 변경할 수 없습니다.

위 코드를 다시 살펴보겠습니다.

  • 참조형 타입은 메소드로 주소값이 전달됩니다.
    메소드가 리턴되고 Max라고 이름이 바뀌었기 때문입니다.
    1
    2
    3
    public void foo(Cat someCat) {
    someCat.setName("Max");
    }
  • 포인터가 가리키는 곳을 변경할 수는 없습니다.
    변경이 가능했다면 myCat은 Rowlf라는 이름일텐데 Max이기 때문입니다.
    여기선 new 연산자를 통해 다른 주소값을 가진 다른 인스턴스가 생성된 것이고 메소드와 같은 라이프 사이클을 갖습니다. 즉, 메소드 리턴과 동시에 사라지게 됩니다.
    1
    2
    3
    4
    public void foo(Cat someCat) {
    someCat = new Cat("Fifi");
    someCat.setName("Rowlf");
    }
    이게 왜 값에 의한 호출인지 아직 명확하지 않아 보입니다.

다음 예시를 보시면 명확하게 이해가 되실 겁니다.

예제2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {

public static void main(String[] args) {
Foo f = new Foo("f");
changeReference(f); // It won't change the reference!
modifyReference(f); // It will modify the object that the reference variable "f" refers to!
}

public static void changeReference(Foo a) {
Foo b = new Foo("b");
a = b;
}

public static void modifyReference(Foo c) {
c.setAttribute("c");
}

}

2-1.

1
Foo f = new Foo("f");

위에서 설명했던 대로 f는 주소값을 가지고 타입이 Foo이고 속성이 f인 객체를 가리킵니다.

2-2.

1
public static void chanageReference(Foo a)

a는 Foo 타입으로 선언되어 있고 null로 초기화 되어 있습니다.

2-3.

1
changeReference(f);

f를 인자로 넘겨주어 a는 f와 동일한 주소값을 갖게 됩니다. 동일한 주소값이니 똑같은 곳을 가리키고 있는 것을 확인할 수 있습니다.

2-4.

1
Foo b = new Foo("b");

b는 주소값을 가지고 타입이 Foo이고 속성이 b인 객체를 가리킵니다.

2-5.

1
a = b;

b의 주소값을 a에 할당하게 되어 a는 b가 가리키는 곳을 가리키게 됩니다.

2-6.

1
2
3
modifyReference(Foo c) { }

modifyReference(f);

f를 인자로 넘겨주어 c는 f의 주소값을 받게 됩니다. 당연히 f가 가리키는 곳을 가리키겠죠?

2-7.

1
c.setAttribute("c");

c가 가리키고 있는 객체의 속성을 c로 변경합니다. c와 f는 동일한 것을 가리키기에 f가 가리키는 객체의 속성은 c가 되는 것입니다.

call by reference 였다면 f의 주소값을 c나 a가 받았겠죠? 그렇다면 위에 a = b가 되었을 때 f도 바뀌었을 것입니다. 하지만 call by value이기 때문에 f, c, a, b 모두 각각 따로 존재하고 값만 전달 받고 있습니다.

Java에서는 모두 값을 전달하는 pass-by-value라는 것을 기억하시면서 이번 글을 마칩니다.

[Reference]


Java에서 null을 다루는 방법

null을 다루는 방법

Java에서 null을 다루는 방법이란 흔히 NullPointerException을 피하도록 코드를 작성하는 것입니다.

먼저 null을 창안한 “토니 호어”는 null을 10억 달러짜리 실수라고 말하였습니다.

왜 그렇게 표현하였을까요?

관계가 없음을 나타내는 null은 모든 타입의 멤버가 될 수 있고 이 때문에 참조 변수 사용 시 널을 확인해야 합니다. 그렇지 않으면 NullPointerException을 보게 될 것이고 이는 실제로 SW 결함 통계에서 상당히 많은 부분을 차지하고 있습니다.

  • null 값을 갖는 변수는 JVM 메모리에서 참조하는 값이 없으면 해시 코드는 항상 0입니다. 즉, 힙 영역에 데이터를 생성하지 않았음.

    그럼 이제 몇 가지 방법을 통해 null을 다루는 방법을 알아보겠습니다.

    1. equals() 사용 시

    1
    2
    3
    4
    5
    6
    7
    Object unknownObject = null;

    // unknownObject.equals("StringLiteral") -> wrong way

    if("StringLiteral".equals(unknownObject)) {
    // TODO
    }

    null을 갖는 객체를 참조하려고 하면 NullPointerException이 발생하기에 위와 같이 사용하는게 안전합니다.

    2. valueOf( )를 더 선호하자. (toString( ) 보다)

    null을 갖는 객체에서 toString()을 호출하면 NullPointerException을 보신 경험이 있을 겁니다.(저만 그런가요?) 그렇기에 null을 반환하는 valueOf()를 사용하는게 좋습니다.

    3. null safe methods와 libraries를 사용하자.

    Null safe methods와 classes 관련 documentation을 보는게 가장 정확합니다. (당연한 소리?!)
    Null safe method의 가장 흔한 예가 StringUtils 메소드로 아래처럼 코드를 작성해도 문제가 없습니다.

    1
    2
    3
    4
    System.out.println(StringUtils.isEmpty(null));
    System.out.println(StringUtils.isBlank(null));
    System.out.println(StringUtils.isNumeric(null));
    System.out.println(StringUtils.isAllUpperCase(null));

    4. null을 반환하는 method 작성을 피하자.

    null이 아닌 EMPTY_LIST와 같이 비어있는 것을 표시하는 것을 사용하여 null 반환하지 않도록 합니다.

    1
    2
    3
    4
    public List getOrders(Customer customer){
    List result = Collections.EMPTY_LIST;
    return result;
    }

    5. @NotNull 이나 @Nullable 어노테이션을 사용한다.

    메소드가 null safe인지 아닌지를 annotation을 사용하여 표시하는 것이 좋습니다. 그래야 컴파일러가 여러분이 미처 확인하지 못 한 또는 굳이 확인할 필요가 없는 부분에서 null check를 하도록 도와줄 수 있습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Employee
    {
    public final int id;
    public final String name;
    public final @Nullable String phone;

    private Employee(int id, String name, @Nullable String phone) {
    ...
    }

    6. 불필요한 autoboxing이나 unboxing 코드를 피하자.

    만약 wrapper 클래스 객체가 null이라면, autoboxing은 NullPointerException에 취약할 수 밖에 없습니다.

    다음 코드에서 doyun이라는 객체가 phjone number가 없다면 null을 리턴하지 않도록 해야 합니다. 그렇지 않다면 NullPointerException을 보게 될 것입니다.

    1
    2
    Person doyun = new Person("Doyun");
    int phone = doyun.getPhone();

    7. 객체 생성 시 규약을 따르고 합리적인 default 값을 정의하자.

    NullPointerException은 대부분 불완전한 정보나 요구되는 의존성을 모두 충족시키지 않고 객체가 생성되었을 때 발생합니다.

    그렇기에 이를 피하는 방법만으로도 null을 다룰 수 있습니다. 예를 들어 Employee라는 객체는 id와 name 값이 없으면 생성될 수 없게 하고 옵션으로 phone number 값을 가질 수 있습니다.
    단 여기서 phone number 값이 없다면 null을 리턴하지 않고 0과 같은 default 값을 리턴하도록 해야 합니다.

    8. DB에서 null 제약 조건을 유지하자.

    도메인 객체(Customers, Order와 같은)를 저장하기위해 DB를 사용하는 경우 DB 자체에서 null 제약 조건을 정의해야 합니다. (데이터의 무결성 보장 때문에)

    DB에서 이런 제약 조건을 유지하는 것만으로 Java code에서 null check를 줄일 수 있습니다.
    이미 DB에서 해당 필드가 null 값을 가질 수 있는지 없는지 확인하기에 Java code 내에서 불필요한 null 체크를 최소화할 수 있습니다.

    9. Null Object Pattern을 사용하자.

  1. 추상 클래스 Employee.java
    1
    2
    3
    4
    5
    6
    7
    public abstract class Employee {
    protected int id;
    protected String name;
    public abstract boolean isNull();
    public abstract int getID();
    public abstract String getName();
    }
  2. 추상 클래스를 확장하는 CurrentEmployee.java (현재 종사하고 있는 직원)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class CurrentEmployee extends Employee {
    public CurrentEmployee(int id, String name) {
    this.id = id;
    this.name = name;
    }

    @Override
    public int getID() {
    return id;
    }
    @Override
    public String getName() {
    return name;
    }
    @Override
    public boolean isNull() {
    return false;
    }
    }
    NullEmployee.java (퇴사하거나 이직한 직원)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class NullEmployee extends Employee {

    @Override
    public int getID() {
    return -1; // default value
    }
    @Override
    public String getName() {
    return "No id in Employee DB";
    }
    @Override
    public boolean isNull() {
    return true;
    }
    }
  3. EmployeeFactory.java (DB에 저장되어 있는 id 조회)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class EmployeeFactory {

    public static final int[] idList = {00001, 00002, 00004};

    public static Employee getEmployee(int id, String name){
    for (int i = 0; i < idList.length; i++) {
    if (idList[i].equalsIgnoreCase(id)){
    return new CurrentEmployee(id, name);
    }
    }
    return new NullEmployee();
    }
    }
  4. NullPatternTest.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class NullPatternTest {
    public static void main(String[] args) {
    Employee employee1 = EmployeeFactory.getEmployee(00001, "Doyun");
    Employee employee2 = EmployeeFactory.getEmployee(00002, "Jieun");
    Employee employee3 = EmployeeFactory.getEmployee(00005, "Doyun");

    // Test
    System.out.println(employee1.getName()); // Doyun
    System.out.println(employee2.getName()); // Jieun
    System.out.println(employee3.getName()); // No id in Employee DB
    }
    }

    이렇게 하면 null 체크를 최소화하여 NullPointerException을 피할 가능성이 높아지고 코드가 간단해집니다.

    다만, 새로운 메소드를 추가할 때마다 Null 객체 클래스에서도 이를 오버라이드해야 하기에 유지보수가 힘들 수도 있습니다.

    이상으로 Null 다루기 편을 마치겠습니다. - 추가할 부분이 있으면 추가하겠습니다.


String VS StringBuffer

String과 StringBuffer 어떤 것을 써야할까?

메모리를 신경쓰는 개발자라면 StringBuffer를 고려해보자.

먼저 String 클래스와 StringBuffer 클래스의 차이점을 알아보겠습니다.

1. 문자열 변경 유무

  • String 클래스의 문자열은 변경 불가능
  • StringBuffer 클래스의 문자열은 변경 가능

2. 메모리 공간 버퍼 존재 여부

  • String: 없음
  • StringBuffer: 내부에 메모리 공간을 조절할 수 있는 버퍼가 존재

3. Thread safe 유무

  • String: non-safe
  • StringBuffer: safe한 구조이기에 멀티스레딩 환경에서도 데이터 무결성을 보장한다.

위 차이점 중에서 StringBuffer의 1, 2번 특징 덕분에 JVM 메모리를 보다 효율적으로 사용할 수 있습니다. 이에 대해 구체적으로 알아보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String getDescription()
{
String description = "Title: ";
description += "String ";
description += "VS ";
description += "StringBuffer\n";
description += "Content: ";
description += "Difference ";
description += "between ";
description += "String ";
description += "and ";
description += "StringBuffer ";
description += "is ";
description += "~~~\n";
return description
}

위와 같은 메소드가 있을 때 JVM 메모리에 생성되는 객체들은 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 순서대로
1. Title:
2. Title: String
3. Title: String VS
4. Title: String VS StringBuffer
5. Title: String VS StringBuffer
Content:
6. Title: String VS StringBuffer
Content: Difference
7. Title: String VS StringBuffer
Content: Difference between
8. Title: String VS StringBuffer
Content: Difference between String
9. Title: String VS StringBuffer
Content: Difference between String and
10. Title: String VS StringBuffer
Content: Difference between String and StringBuffer
11. Title: String VS StringBuffer
Content: Difference between String and StringBuffer is
12. Title: String VS StringBuffer
Content: Difference between String and StringBuffer is ~~~

String 객체가 담고 있는 문자열은 변경이 불가능 하기에 모든 라인마다 JVM에 새로운 String 객체가 생성됩니다.

StringBuffer 객체는 문자열 버퍼가 존재하고 있어서 하나의 객체에서 작업이 이루어지므로 JVM 메모리를 보다 효율적으로 사용할 수 있습니다.
위와 같은 경우보다 훨씬 더 많은 작업을 요구한다면 StringBuffer를 사용하는게 성능면에서 상당히 유리할 것입니다.

위의 코드를 StringBuffer로 변환하면서 여기서 설명을 마치겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String getDescription()
{
StringBuffer description = new StringBuffer("Title: ");
description.append("String ");
.append("VS ");
.append("StringBuffer\n");
.append("Content: ");
.append("Difference ");
.append("between ");
.append("String ");
.append("and ");
.append("StringBuffer ");
.append("is ");
.append("~~~\n");
return description.toString();
}

  • append 메소드는 연달아 사용히 가능한데 이는 자기 자신의 객체를 다시 반환하기 때문입니다.

JVM과 친해지기

JVM이 뭐죠?

JVM은 Java Virtual Machine으로 저희가 작성한 .class 자바 코드(애플리케이션)을 동작시켜줍니다.

각 OS에 맞는 JVM이 존재하여 다른 OS 환경에서도 동일하게 자바 코드가 실행되도록 합니다.

JVM은 어떻게 동작하나요?

JVM이 어떻게 동작하는지 보기 전에 JVM 구조를 먼저 살펴보겠습니다.

JVM 구조

크게 Class Loader, Execution Engine, Runtime Data Area로 구성됩니다. 좀 더 자세히 알아보겠습니다.

Class Loader

.class 파일(들)과 CLASSPATH 환경변수로 지정한 jdk/lib에 있는 class 파일들 그리고 .jar 파일까지 모두 JVM 위에 올려놓는 역할을 수행합니다.

Execution Engine

바이트 코드를 명령어 단위로 읽어서 실행하는 역할을 합니다. 여기서 바이트 코드는 Class Loader에 올려진 class 파일들이며 애초에 .java → .class(바이트 코드)로 컴파일한 이유는 JVM이 읽을 수 있게 바이트 코드로 변환한 것이고 실행을 위해서는 기계가 읽을 수 있게 변환하는 작업이 필요합니다.

*바이트 코드는 사람에 더 친숙하다고 볼 수 있습니다.(물론 기계어 보단…친숙하실 겁니다. 무려 hex editor로 바이트 코드를 읽을 수 있다구요!)

기계가 읽을 수 있도록 다시 번역을 해야 하는 작업이 여기서 이루어집니다. 두 가지 방식이 존재합니다.

인터프리터(Interpreter)

흔히 Python, JavaScript 등이 이 방식으로 실행됩니다. 명령어를 하나씩 읽어 해석하고 실행합니다. (한 번만 실행되는 코드는 보통 이 방식으로 진행됩니다.)

인터프리터 방식으로 실행하다가 적절한 시점에 바이트 코드 전체를 컴파일하여 네이티브 코드로 변경하고 이후에는 인터프리팅하지 않고 네이티브 코드로 직접 실행합니다.

JIT(Just-In-Time)

적절한 시점은 이 코드가 자주 실행되는 체크를 하면서 판단하게 됩니다.
또한 네이티브 코드는 캐시에 보관하기에 여러 번 수행되는 코드는 이 방식으로 빠르게 실행시킬 수 있습니다.

Runtime Data Area

JVM이 OS위에서 실행되면서 할당받는 메모리 영역입니다.

PC register / JVM Stack / Native Method / Heap / Method

모든 Thread가 공유하는 영역은 (JVM이 시작될 때 생성되는) Heap과 Method 영역입니다. 그외는 각자 자신만의 영역을 가지고 있습니다. (독립적으로 수행되는 thread를 생각하면 이해가 되실겁니다.)

PC register

현재 수행 중인 JVM 명령 주소가 담겨있습니다.

JVM Stack

Stack Frame이라는 구조체를 저장하는 스텍입니다.

Stack Frame은 메서드가 수행될 때마다 생성되고 종료될 때 제거됩니다. 그 안에는 local variable array, operand stack, reference of runtime constant pool이 담겨있습니다.

Local variable array는 0번째 인덱스에 메서드를 호출한 클래스(this)가 담겨있고 그 이후로는 메서드 파라미터, 지역변수가 담겨있습니다.

Operand stack은 메서드의 호출 결과를 push, pop하면서 실제 작업을 수행합니다.

Reference of runtime constant pool은 현재 실행 중인 메서드가 속한 클래스의 런타임 상수 풀에 대한 참조가 담겨있습니다.

Heap

객체나 인스턴스가 생성되어 저장되는 공간으로 GC의 대상이 되기에 성능 튜닝은 보통 이곳에서 이루어집니다.

Method

JVM이 읽어들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드, 메서드의 정보와 static 변수, 메서드의 바이트 코드 등 중요한 정보를 보관하는 영역입니다.

이 중 Runtime Constant Pool 영역이 내부에 따로 존재합니다. 핵심적인 역할을 담당하기 때문입니다. 이곳에서 각 클래스와 인터페이스의 상수뿐만 아니라 메서드와 필드에 대한 모든 레퍼런스까지 담고 있어 이곳을 통해 메소드나 필드의 주소를 찾고 참조를 합니다.

JVM 동작 과정

.java 파일이 javac(자바 컴파일러)를 통해 .class 파일(바이트코드)로 변환되고 이로써 JVM이 읽을 수 있게 됩니다. 이런 class 이외에 필요한 다른 class 들을 Class Loader가 올리면 Execution Engine이 이를 실행시킵니다. Runtime Data Area는 Class Loader와 Execution Engine과 상호작용을 합니다.

[Reference]