결론부터 말하면,

 

아래처럼 코드 처럼 JpaRepository를 상속받은 인터페이스가 있다고 할 때,

public interface BoardJpaRepository extends JpaRepository<BoardEntity, Long>

"BoardJpaRepositoryImpl" 이라는 클래스를 만들면, Spring이 해당 레포지토리의 "커스텀 구현체" 라고 인식합니다.

 

---

 

Spring 스터디를 진행하면서 과제를 받았습니다.

 

과제 내용은 "JDBC 로 간단하게 CRUD 구현하고, JPA로 바꿔보기"

 

저는 순수 JDBC / Spring JDBC / JPA 순으로 구현하려고 했습니다.

 

DB접근 방식만 바뀌고, 동작하는 메서드는 동일하기 때문에

 

BoardRepository 라는 추상화된 레포지토리 인터페이스를 두고, JDBC / Spring JDBC / JPA가 구현하도록 했습니다.

클래스 다이어그램은 아래와 같습니다.

 

JDBC / Spring JDBC 까지 구현할 때는 문제가 없었습니다.

BoardService에서는 BoardRepository에만 의존하고, Profile에 따라 구현체를 주입해주기때문에 Service나 Controller코드에서도 변경이 없었습니다.

 

그런데 JPA로 구현할 때가 문제였습니다.

 

먼저, BoardJpaRepository는 JpaRepository를 상속받게 하였고

public interface BoardJpaRepository extends JpaRepository<BoardEntity, Long> {
    // 필요한 메서드들..
}

 

 

BoardJpaRepositoryImpl 클래스는 아래와 같습니다.

@Profile("jpa")
@Repository
@RequiredArgsConstructor
public class BoardJpaRepositoryImpl implements BoardRepository {

    private final BoardJpaRepository boardJpaRepository;

    @Override
    public Board findById(Long id) {
        return boardJpaRepository.findById(id)
                .map(e -> e.toBoard())
                .orElse(null);
    }
    
    // 기타 BoardRepository 인터페이스 구현...
}

 

 

그런데 위처럼 구현하고 서버를 켰더니..

 

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

 

문구와 함께

 

 

boardJpaRepositoryImpl 에서 순환참조가 일어난다고 하는 것이었습니다. 제일 처음에 말한 결론을 모른 상태로 문제를 해결하려니 많은 시간이 소요됐습니다.

 

https://docs.spring.io/spring-data/jpa/reference/repositories/custom-implementations.html#repositories.single-repository-behavior

 

Custom Repository Implementations :: Spring Data JPA

The approach described in the preceding section requires customization of each repository interfaces when you want to customize the base repository behavior so that all repositories are affected. To instead change behavior for all repositories, you can cre

docs.spring.io

스프링 공식 문서에서

 

사진과 같은 내용을 확인할 수 있습니다.

결국에는 JpaRepository를 상속받은 인터페이스명 + Impl을 붙인 클래스는 Spring이 자동으로 커스텀 구현체로 감지한다는 것이었고,

BoardJpaRepository프록시를 생성하려고 하니, 커스텀 구현체(BoardJpaRepositoryImpl)를 생성해야 하고, BoardJpaRepositoryImpl는 생성자에서 BoardJpaRepository를 주입받도록 정의되어있으니까, 생성 -> 주입 -> 생성 -> ... 의 무한 루프에 빠지게 되는 것이었습니다.. 단순히 BoardJpaRepositoryImpl 같은 네이밍이 아닌 다른 이름, 저는 BoardJpaCustomRepositoryImpl 로 바꾸어 해결했습니다.

 

굳이 BoardJpaRepositoryImpl를 써야겠다고 하면 공식문서에는 @EnableJpaRepositories 의 repositoryImplementationPostfix 옵션을 바꾸거나, @Bean 팩토리 메서드를 직접 만들라고 합니다.

https://boj.ma/9502/t

 

9502번: Bones’s Battery

 

boj.ma

 

모든 학교 간 이동을 가능하게 하는 최소 배터리 용량을 찾고싶은 문제입니다.

충전은 최대 K번 할 수 있고 시작시 용량이 0이기 때문에 시작할때 반드시 한 번 충전을 해야합니다.

 

용량을 최소한으로 소모하여 이동하는게 이득이기 때문에 어떤 두 정점간 최단경로로 이동하는것이 최적입니다.

 

문제에서 원하는 값은 "최대 K번 충전하여 모든 학교간 이동을 가능하게 하는 충전량의 최소값" 입니다.

완탐을 떠올려 보면

1회 충전 시 충전량이 1일 때 최대 K번 충전해서 모든 학교간 이동 가능하냐?

1회 충전 시 충전량이 2일 때 최대 K번 충전해서 모든 학교간 이동 가능하냐?

1회 충전 시 충전량이 3일 때 최대 K번 충전해서 모든 학교간 이동 가능하냐?

1회 충전 시 충전량이 4일 때 최대 K번 충전해서 모든 학교간 이동 가능하냐?

...

1회 충전 시 충전량이 X일 때 최대 K번 충전해서 모든 학교간 이동 가능하냐?

처럼 결정 문제로 바꿀 수 있고, 위 결정문제에 대한 정답은 단조성을 보이기 때문에 즉, 특정 용량 이후로는 항상 가능하다는 대답이 나오기 때문에 이분탐색으로 최적화가 가능합니다.

 

모든 정점쌍 간 최단경로를 구해야하고, N이 작기 때문에 플로이드 워셜로 최단경로를 구할 수 있습니다.

 

시간복잡도는 이분탐색으로 가능한 구간에 매 탐색마다 플로이드를 돌리기 때문에

$O(N^3 log_210^{11})$ 이 됩니다.

 

코드 (C++)

#include <bits/stdc++.h>
#include <random>
#include <unordered_map>
#include <unordered_set>
#pragma GCC optimize("O3")
#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")
#define endl "\n"
#define NM 15*15+10
#define MAX 150010
#define BIAS 1048576
#define X first
#define Y second
#define INF 0x3f3f3f3f
#define LINF 0x3f3f3f3f3f3f3f3f
#define FOR(i) for(int _=0;_<(i);_++)
#define pii pair<int, int>
#define pll pair<ll, ll>
#define all(v) v.begin(), v.end()
#define fastio ios_base::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL)
using namespace std;
using ll = long long;
using ull = unsigned long long;

int dx[8] = { 1, 0, -1, 0, 1, 1, -1, -1 };
int dy[8] = { 0, 1, 0, -1, 1, -1, 1, -1 };


int main() {
	fastio;

	int t;
	cin >> t;
	while (t--) {
		int n, K, m;
		cin >> n >> K >> m;
		vector<vector<ll>> dist(n, vector<ll>(n, LINF));

		for (int i = 0; i < n; i++) dist[i][i] = 0;

		for (int i = 0; i < m; i++) {
			ll a, b, c;
			cin >> a >> b >> c;
			dist[a][b] = min(dist[a][b], c);
			dist[b][a] = min(dist[b][a], c);
		}

		for (int k = 0; k < n;k++) {
			for (int i = 0; i < n; i++){
				for (int j = 0; j < n; j++) {
					dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
				}
			}
		}

		auto ok = [&](ll x)->bool {
			vector<vector<ll>> tmp(n, vector<ll>(n, INF));
			for (int i = 0; i < n; i++) {
				for (int j = 0; j < n;j++) {
					if (i == j) tmp[i][j] = 0;
					else if (dist[i][j] <= x) tmp[i][j] = 1;
				}
			}
			for (int k = 0; k < n;k++) {
				for (int i = 0; i < n; i++) {
					for (int j = 0; j < n; j++) {
						tmp[i][j] = min(tmp[i][j], tmp[i][k] + tmp[k][j]);
					}
				}
			}

			for (int i = 0; i < n; i++) {
				for (int j = 0; j < n; j++) {
					if (tmp[i][j] > K)  return false;
				}
			}

			return true;
		};

		ll l = 0, r = LINF;
		ll ans = r;

		while (l <= r) {
			ll mid = l + r >> 1;
			if (ok(mid)) {
				ans = mid;
				r = mid - 1;
			}
			else {
				l = mid + 1;
			}
		}
		cout << ans << endl;

	}
	

	return 0;
}

 

'알고리즘 문제 풀이' 카테고리의 다른 글

백준 33543 둘이 한 팀  (0) 2025.03.03
백준 2001 보석 줍기  (2) 2024.08.31
백준 17943 도미노 예측  (0) 2024.07.18
백준 7346 유전자 함수  (0) 2024.05.16
백준 23889 돌 굴러가유  (0) 2024.05.16

https://boj.ma/33543/t

 

33543번: 둘이 한 팀

 

boj.ma

문제 요약은

\[
\sum_{i=1}^{N} \max\Bigl( A_i + \text{A증가량}, \; B_i + \text{B증가량} \Bigr)
\]
를 쿼리마다 구하는 문제입니다.

 

당연히 일일히 업데이트 치고, 매번 합을 구하게 되면 시간초과입니다.

$\max(x, y)$ 를 생각해봅시다. $\max(x, y)$ 는  $\max(x, y) = y + \max(x - y, 0)$ 로 나타낼 수 있습니다.

따라서 우리가 구하려는 $ \max( A_i + \text{A증가량}, \; B_i + \text{B증가량})$ 를 바꾸면,

$\max(A_i + \text{A증가량}, B_i + \text{B증가량}) = B_i + \text{B증가량} + \max((A_i + \text{A증가량}) - (B_i + \text{B증가량}), 0)$ 가 됩니다.

따라서 원래 수식을 아래와 같이 나타낼 수 있습니다.

\[\sum_{i=1}^{N} (B_i + \text{B증가량}) + \sum_{i=1}^{N} \max((A_i - B_i) + \text{A증가량 - B증가량}, 0)\]

 

앞쪽 식은 초기 B배열의 전체 합과 쿼리마다 B를 업데이트 칠 때마다 B증가량에 더해주면 $O(1)$에 구할 수 있습니다.

뒤쪽 식은 $(A_i - B_i)$ 를 미리 계산해둔 배열을 만들어두고, 정렬후 누적합 배열을 만들어 줍니다.

이후 각 쿼리마다 A증가량 - B증가량은 $O(1)$ 에 업데이트 하고, $ \sum_{i=1}^{N} \max((A_i - B_i) + \text{A증가량 - B증가량}, 0) $ 을 이분탐색으로 빠르게 구해줍니다.

 

$ A_i - B_i $ 배열을 정렬하고 누적합을 구해놓는다고 했습니다. 순서가 원래 배열의 순서에서 깨지더라도 상관없습니다.

합은 어차피 $A_i - B_i + \text{A증가량 - B증가량}$ 이 0보다 큰것들만 더해지기 때문에 0보다 큰게 몇개인지만 관심가지면 됩니다.

 

 

따라서  $(A_i - B_i) + \text{A증가량 - B증가량}  \geq 0$ 가 되는 $i$를 이분탐색으로 빠르게 찾고, 

$ N - i $ 개 원소에 대해 $ A_i - B_i + \text{A증가량 - B증가량} $ 의 합은  

$$ \text{psum}[N] - \text{psum}[\text{idx}] + \text{(A증가량 - B증가량)}  \times (N - \text{idx}) $$  

으로 나타낼 수 있습니다.

최종 수식은 아래와 같습니다.

\[
\text{sumB} + n \times \text{B증가량} + \left[ (\text{ psum }[n] - \text{ psum }[\text{idx}]) + \text{ (A증가량 - B증가량) } \times (n - \text{idx}) \right]
\]

 

이제 수식대로 구현해주면 됩니다.

 

코드는 생략합니다.

 

 

 $\max(x, y) = y + \max(x - y, 0)$ 아이디어가 중요한것 같습니다.

'알고리즘 문제 풀이' 카테고리의 다른 글

백준 9502 Bones’s Battery  (0) 2025.03.04
백준 2001 보석 줍기  (2) 2024.08.31
백준 17943 도미노 예측  (0) 2024.07.18
백준 7346 유전자 함수  (0) 2024.05.16
백준 23889 돌 굴러가유  (0) 2024.05.16

대량의 데이터를 페이징할 때 OFFSET 방식은 페이지가 뒤로 갈수록 성능이 저하되는 문제가 발생합니다.

이 글에서는 MySQL의 Covering Index를 활용하여 페이징 성능을 최적화하는 방법을 실험하고, 기존 방식과 비교하여 성능 향상을 검증합니다.

마지막으로 추가적인 최적화 방안까지 고민해봅니다.

 

목차

  1. 문제 상황
  2. 성능 이슈 분석
  3. 최적화 방안: Covering Index 활용
  4. 성능 개선 원리
  5. 결론
  6. 정말 끝일까?

1. 문제 상황

이전 프로젝트에서 자기소개서 목록을 페이징하는 로직을 Page 인터페이스로 구현했는데, 데이터가 대량일 경우 성능이 떨어진다는 것을 발견했고 리팩토링할 필요가 있었습니다.

1.1 기존 구현 코드

Controller

@GetMapping("/essays") // 전체 자소서 목록 불러오기
public ResponseEntity<?> showEssays(@AuthenticationPrincipal CustomMemberDetails member, Pageable pageable) {
    Page<EssayDto> list = essayService.findEssayByUserId(member.getUsername(), pageable);
    return BaseResponse.okWithData(HttpStatus.OK, "에세이 목록 불러오기 성공", list);
}

 

Service

@Service
public class EssayService {
    @Override
    public Page<EssayDto> findEssayByUserId(String userId, Pageable pageable) {
        Member member = memberRepository.findByUserId(userId)
            .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저"));
        return essayRepository.findByMember(member, pageable).map(EssayDto::from);
    }
}

Repository

Page<Essay> findByMember(Member member, Pageable pageable);

1.2 데이터베이스 구조

테이블 구조와 적용한 인덱스는 다음과 같습니다.

CREATE INDEX idx_user_id_id ON Essay(user_id ASC, id DESC);

2. 성능 이슈 분석

2.1 문제점 확인

페이징을 위해 사용한 기존 쿼리는 다음과 같습니다.

SELECT * FROM essay
WHERE user_id = 1
ORDER BY id DESC
LIMIT 30 OFFSET 90;

4페이지 조회 시 실행 속도: 0.000초

 

그런데, 50,000번째 페이지를 조회하면 어떻게 될까요?

SELECT * FROM essay
WHERE user_id = 1
ORDER BY id DESC
LIMIT 30 OFFSET 1499970;

50,000페이지 조회 시 실행 속도: 7.6초

 

2.2 원인 분석

실행 계획 분석

id: 1
select_type: SIMPLE
table: essay
partitions: NULL
type: ref
possible_keys: idx_user_id_id
key: idx_user_id_id
key_len: 8
ref: const
rows: 4865398
filtered: 100.00
Extra: NULL

 

key로 인덱스를 사용하고 있지만 OFFSET이 커질수록 속도가 느려짐.

문제점: OFFSET이 클 경우, MySQL이 해당 OFFSET까지의 데이터를 모두 스캔하고 버린 후, 필요한 데이터를 가져오는 방식이 비효율적인 것입니다.

왜 그럴까요?

 MySQL의 기본 스토리지 엔진인 InnoDB는 테이블마다 Clustered Index를 자동으로 생성합니다.

  • PK 기준으로 정렬된 Clustered Index
  • Clustered Index는 leaf node의 값으로 행 데이터를 가집니다.

essay 테이블에는 essay_id를 기준으로 하는 Clustered Index가 생성되어 있고, 데이터를 가집니다.

Primary Key를 이용한 조회는 자동으로 생성된 Clustered Index로 수행되는 것입니다.

 

그래서 이렇게 PK인 id를 기준으로 조회를 하면 0.000초만에 조회를 할 수 있는 것입니다.

select * from essay
where id = 8888590;

수행시간 : 0.00초

 

쿼리 실행 계획을 봐도

id: 1
select_type: SIMPLE
table: essay
partitions: NULL
type: const
possible_keys: PRIMARY
key: PRIMARY
key_len: 8
ref: const
rows: 1
filtered: 100.00
Extra: NULL

 

key가 Primary로 되어있는 것을 볼 수 있습니다.

근데 페이징을 하며 조회하는 쿼리에서는 Secondary Index인 idx_user_id_id로 조회합니다.

 

Secondary Index 특징
- 인덱스 컬럼 데이터
- 데이터에 접근하기 위한 포인터

실제 데이터는 Clustered Index가 갖고있음
결국 Clustered Index에 접근하기 위한 포인터임

PK를 통해 조회하면 Clustered Index로 데이터를 빠르게 찾을 수 있습니다.

그렇다면 Secondary Index조회 처리는 어떻게 되는 걸까요?

Secondary Index는 데이터에 접근하기 위한 포인터만 갖고 있고, 실제 데이터는 Clustered Index가 갖고 있습니다.

따라서 데이터를 찾는 흐름이

1. Secondary Index에서 데이터에 접근하기 위한 포인터를 찾은 뒤

2. Clustered Index에서 데이터를 찾는다.

이렇게 되고,

 

현재 구현된 페이징 방식의 흐름은

1. idx_user_id_id 에 생성된 Secondary Index에서 id(essay_id)를 찾습니다.

2. Clustered Index에서 essay 데이터를 찾습니다.

3. offset 1499970을 만날 때까지 반복하여 skip합니다.

4. 30개 데이터를 추출

이렇게 되고, 결국에는 인덱스 트리(B+ Tree)를 두 번 타게 되는 것입니다.

select * from essay
where user_id = 1
order by id desc
limit 30 offset 1499970;

 

 

2.3 해결 방안 탐색

현재 Secondary Index에는 user_id와 id(essay_id)만 포함된다.

따라서 Secondary Index에서 필요한 N개에 대해서만 id(essay_id)를 추출하고, 그 N건에 대해서만 Clustered Index에 접근하면 되지 않을까?

 

아래 쿼리를 봅시다

select id, user_id from essay
where user_id = 1
order by id desc
limit 30 offset 1499970;

수행시간 : 0.500초

 

이것도 실행계획을 살펴보면

id: 1
select_type: SIMPLE
table: essay
partitions: NULL
type: ref
possible_keys: idx_user_id_id
key: idx_user_id_id
key_len: 8
ref: const
rows: 4865398
filtered: 100.00
Extra: Using index

key 에서 사용하는 인덱스는 동일한데, Extra를 보면 Using Index가 추가됐습니다.

인덱스만을 이용해서 데이터를 뽑았다는 거죠. 이렇게 인덱스의 데이터만으로 조회할 수 있는 인덱스를 Covering Index라고 합니다.


3. 최적화 방안: Covering Index 활용

3.1 Covering Index란?

인덱스 자체만으로 쿼리의 모든 데이터를 처리할 수 있는 인덱스.

데이터(Clustered Index)를 읽지 않고, 인덱스(Secondary Index)가 포함된 정보만으로 쿼리가 가능한 인덱스

3.2 최적화 적용

Covering Index 적용 전후 비교

-- 기존 방식
SELECT * FROM essay
WHERE user_id = 1
ORDER BY id DESC
LIMIT 30 OFFSET 1499970;

실행 속도: 7.6초

-- Covering Index 활용
SELECT * FROM (
    SELECT id FROM essay
    WHERE user_id = 1
    ORDER BY id DESC
    LIMIT 30 OFFSET 1499970
) t LEFT JOIN essay ON t.id = essay.id;

실행 속도: 0.32초

쿼리 실행계획을 보면

 

*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: <derived2>
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1500000
filtered: 100.00
Extra: NULL
*************************** 2. row ***************************
id: 1
select_type: PRIMARY
table: essay
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 8
ref: t.id
rows: 1
filtered: 100.00
Extra: NULL
*************************** 3. row ***************************
id: 2
select_type: DERIVED
table: essay
partitions: NULL
type: ref
possible_keys: idx_user_id_id
key: idx_user_id_id
key_len: 8
ref: const
rows: 4865398
filtered: 100.00
Extra: Using index

서브쿼리를 실행하는 과정에서 파생 테이블(Derived)이 생기지만 그 과정에서 Using Index로 Covering Index가 사용됐습니다. 작은 규모의 파생 테이블과 조인하여 N건에 대해서만 Clustered Index에서 가져오기 때문에 빠르게 처리하는 것입니다.

3.3 성능 개선 결과

방식 실행 시간
기존 방식 7.6초
Covering Index 사용 0.32초
성능 향상률 약 95.79%

 

커버링 인덱스를 사용하면 불필요한 PK Lookup을 최소화하여 성능이 크게 향상됨.


4. 성능 개선 원리

  • Secondary Index를 활용하여 인덱스 스캔 수행
  • 서브쿼리를 활용해 Covering Index를 통해 필요한 id 목록만 가져오고, 이후 조인하여 데이터를 조회

5. 결론

  • 기존 OFFSET 기반 페이징 방식은 데이터가 많아질수록 비효율적.
  • Covering Index를 활용하여 성능을 최적화 가능.
  • 결과적으로 기존보다 95.79% 성능 향상 효과를 볼 수 있음.

대량 데이터 페이징 시 Covering Index를 적극 활용하자.

 


6. 정말 끝일까?

이제 50,000번 페이지를 조회해도 빠르게 처리가 잘 됩니다.

정상적으로 서비스를 하는데 무리도 없어보이죠.

 

근데 정말 이게 끝일까요?

대부분의 경우에서 별 문제가 없을 거긴 합니다.

 

만약에 50,000번 페이지가 아닌 300,000번 페이지를 조회한다면?

Secondary Index만 탄다고 해도, 결국 offset 만큼 Index Scan이 필요하게 됩니다.

데이터에 접근하지 않더라도 offset이 늘어날 수록 느려질 수 밖에 없는 것입니다.

 

해결 방법은 다양하지만 아래와 같은 해결 방법이 있습니다.

 

  1. 데이터를 한 번 더 분리한다
    • 1년 단위로 테이블 분리
      • 개별 테이블의 크기를 작게 만들고
      • 각 단위에 대해 전체 게시글 수를 관리한다.
    • offset을 인덱스 페이지 단위로 skip하는 것이 아니라 1년 동안 작성된 게시글 수 단위로 즉시 skip
      • 조회하고자 하는 offset이 1년 동안 작성된 게시글 수보다 크다면
        • 해당 개수만큼 즉시 skip하기
        • 더 큰 단위로 skip 수행 가능
      • Application에서 코드 처리 필요함
  2. 30만번째 페이지를 조회하는 것이 정상적인 사용자일까?
    • 정책으로 막아버리기
      • 게시글 목록 조회는 50,000번 까지만 가능
    • 시간 범위 또는 텍스트 검색 기능
      • 더 작은 데이터 집합 안에서 페이징 수행
  3. 무한 스크롤
    • 페이지 번호 방식에서는 동작 특성 상, 뒷 페이지로 갈수록 속도가 느려질 수밖에 없습니다.
    • 무한 스크롤은 아무리 뒷 페이지로 가더라도 균등한 조회 속도를 가집니다.

위 내용들은 추후 구현해보고 성능 측정도 해보겠습니다.

 


인프런 널널한 개발자 강사님의 기초 탄탄! 독하게 시작하는 Java - Part2 : OOP와 JVM의 강의 중 JVM내용 정리


 

Class Loader는 우리가 직접 만들수도 있다고 합니다. C/C++로 구현하지 않고 JAVA로도 직접 구현해서 사용할 수 있다고 하네요.

Class Loader에 대해 이해하기 위해 .class 파일과 바이트 코드에 대해 짚고 넘어갑니다.

 

.class 파일은 .java파일을 컴파일 했을때 생성되는 자바 바이트 코드입니다.

 이것을 Class Loader가 Loading하는 것입니다. 윈도우로 치면 .exe, .dll 같은 확장자를 가진 실행파일(PE)이 JVM에서는 .class파일인 겁니다.

class 파일의 구조는 아래와 같다고 합니다.

ClassFile {
    u4 				magic;
    u2 				minor_version;
    u2 				major_version;
    u2 				constant_pool_count;
    cp_info 			constant_pool[constant_pool_count-1];
    u2 				access_flags;
    u2 				this_class;
    u2 				super_class;
    u2 				interfaces_count;
    u2 				interfaces[interfaces_count];
    u2 				fields_count;
    field_info 			fields[fields_count];
    u2 				methods_count;
    method_info 		methods[methods_count];
    u2 				attributes_count;
    attribute_info 		attributes[attributes_count];
}

 

위에서 부터

u4는 unsigned 캐릭터 아마 4바이트

밑으로 minor_version, major_version 정보가 나와있고

그 밑에 중요한 constant_pool_count 얘기가 나옵니다. 아무튼 이것저것 this class, super class, field, method, 이런정보가 담겨져 있구나 라는걸 알 수 있씁니다.

 

 

 

사진은 대표적인 Hello World! 코드를 컴파일한 .class 을 16진수 편집기로 열어본 것입니다.

 

맨 앞의 CA FE BA BE 이 4바이트가 위에서 u4에 해당하는 거고, 그 뒤로 major/minor 버전, constant pool 등등 정보가 담겨져 있는 것입니다. 근데 뭐 저거만 보고 리버스 엔지니어링을 하는 것은 힘들기 때문에 보통 도구를 이용해서

.class 파일을 분석하기도 한다고 합니다.

 

IntelliJ에서도 바이트코드를 볼 수가 있는데

바이트 코드 보는법은 생략하고 저런식으로 더 쉬운 형태로 변환하여 .class 파일을 분석할 수 잇다고 합니다.

저런 거로 디컴파일도 할 수 있다고 하네요.

 

아무튼

 

Hello World를 출력하는 .java 파일이 java byte code로 바뀌고 그걸 JVM이라는 프로세스가 실행시켜주는데,

근데 실질적으로 실행은 CPU가 하는 것입니다.

자바로 프로그램을 개발하게 되면 결국은 자바 소스코드 말고,

.class 파일에 들어있는 형태인 자바 바이트 코드의 스트림 형태로 변환이 되어서 로딩되고 적재된 다음에 어쩌구저쩌구 쭈욱 가는겁니다.

 

 

 

자바 바이트코드의 실행의 근거가 되어주는 것들은 JVM에서의 명령들입니다. 근데 JVM 자체가 가상 머신이지 진짜 머신(CPU)는 아닙니다. 그래서 결과적으로 그것들(바이트코드들)을 CPU가 인식할 수 있는 Native코드로 바꾸어 주어야 합니다. 그래서 그 과정에서 사용되는 컴파일러가 등장합니다. 그래서 컴파일러가 두 번 등장하는데

첫 번째로, .java파일을 .class 파일로 바꾸는 컴파일러.

두 번째로, .class파일을 CPU가 이해할 수 잇는 Native 코드로 바꾸는 컴파일러

네이티브로 바꿔주는 컴파일러는 두 놈이 있습니다.

 

출처: https://medium.com/webeveloper/jvm-java-virtual-machine-architecture-94b914e93d86

JVM의 3가지 영역 중 Execution Engine에 해당되는 부분에서 Interpreter와 JIT Compiler 입니다.

 

JIT컴파일러(Just-In-Time) 는 JVM이 바이트코드 레벨에서 분석을 해서 예를들어 반복문을 돈다고 하면 반복문 안의 코드는 특정 횟수 만큼 반복이 될 것입니다 이런 반복되는 부분에대해 JIT가 미리 컴파일을 해놓습니다.

평상시에는 Interpreter가  바이트코드를 일정 단위 뜯어와서 CPU에 번역해서 연산시키고 합니다. 근데 이걸 매번 변환하는 과정을 거친다는 것은 성능을 떨어뜨리는 원인이 됩니다.

그래서? JIT가 자주 사용되는 부분을 미리 번역해놓고 재사용하기 때문에 JIT로 인해 JVM의 성능이 극단적으로 상승했다고 합니다.

 


 

Class Loader

클래스로더의 역할을 생각해보면 하는 일도 많고 중요하기도 하지만 생각보다 이걸 건드리거나 할 일은 많이 없다고 합니다. 따라서 클래스로더가 무슨일을 하는지 이론적으로만 알아도 된다고 합니다.

 

우리가 작성한 JAVA파일을 class 파일로 컴파일이 되고, class파일 안에 있는 자바 바이트코드를 JVM이 뜯어와서 실행을 하게 됩니다.

중요한 점은 바이트 코드의 본질은 결국에는 Method라는 것입니다.

 

클래스 로더가 하는 일은 아래와 같습니다.

 

1. 이름을 알고 있는 특정 클래스에 대한 정의(Byte Stream)를 가져오는 역할을 수행

  -> 가져온다라는 의미를 생각해 보면 어디 다른곳에서 가져올 수 있다는 의미입니다. 네트워크를 통해 퍼온다거나 할 수 있습니다.

2. 부트스트랩 클래스 로더

  - JVM에서 라이브러리로 취급(rt.jar, tools.jar)되는 것들을 로드 (핫스팟에서는 C++로 구현)

3. 플랫폼 클래스 로더 (기존 확장 클래스 로더)

  - 클래스 라이브러리 로드

4. 애플리케이션 클래스 로더

  - sun.misc.Launcher$AppClassLoader를 의미

 

 

클래스가 로딩되는 과정

 

개발자에게는 3가지 시점이 있습니다.

1. 컴파일타임

2. 링크타임

3. 런타임

그런데 자바에서는 이렇게 구분하는 개념이 없습니다. build를 하면 컴파일타임을 말하고 실행을 하면 링크 + 런타임을 묶는다고 합니다.

 

출처 : 강의자료 일부

그래서 클래스를 로딩하겠다 하면 JVM이 인식할 수 있게 하는 자바 바이트코드 스트림 덩어리로 바꾸는 과정을 로딩 단계라고 합니다. 위 그림에서는 Loading -> Linking 까지 입니다.

그리고 사용단계는 그 아래쪽인데 사용단계 수준에서 인스턴스화를 진행합니다. 즉, new 연산이 등장하는거죠 그래서 Using 박스 안에 Initializaion(초기화) 가 있는 것입니다.

그리고 Unloading은 GC를 통해 메모리가 해제되는 것입니다.

 

Java 클래스 로딩

- 클래스 로딩 및 링킹 과정이 모두 런타임에 이루어짐

- 실행 성능이 일부 저하될 수 있으나 높은 확장성과 유연성을 제공하는 근간

  - 인터페이스만 맞으면 Runtime에 구현 클래스를 결정하지 않을 수 잇음

  - 클래스 로더는 실행할 프로그램 코드를 네트워크로 수신하는 것도 가능

- 해석(Resolution) 단계는 동적 바인등(혹은 늦은 바인딩)을 지원할 목적으로 초기화 후로 지연될 수 있음

 

 

로딩 단계에서 가장 먼저 하는일, 검증.(Verification)

- JVM 명세가 정하는 규칙과 제약을 만족하는지 확인합니다.

  - 파일 형식(.class)

  - 메타데이터

  - 바이트코드

  - 심벌 참조

- 보안위협에 대한 검증 포함

  - 바이트코드 검증 시 함께 확인

 

준비 및 해석단계(Preparation, Resolution)

- java.lang.Class 인스턴스(메타 데이터)가 힙 영역에 생성되고 클래스 변수(정적 멤버) 메모리를 0으로 초기화

  -> 로드되는 클래스의 인스턴스는 Using 단계에서 힙 사용

  -> final 선언된 변수는 코드에서 정의한 초깃값으로 정의(0이 아닐수 있다)

- 생성자 호출 전 상태(new 연산 전 단계)

  -> 필드(인스턴스 변수) 초깃값은 생성자 호출 시 정의됨

  -> 정적 필드에 초깃값 할당

- 해석은 상수 풀의 심벌 참조를 직접 참조로 대체하는 과정

 

 

사용단계

Heap 영역에 객체 생성 (new 연산 실행) 

- JVM은 객체 저장을 위한 메모리 공간을 확보 후 0으로 초기화 (단, 객체 헤더 제외)

- 객체 초기화를 위한 구성설정 실시

  -> 클래스 이름 및 메타 정보 확인 방법

  -> 객체에 대한 해시코드

  -> GC 세대 나이

- 생성자 호출

 

'CS > JAVA' 카테고리의 다른 글

JVM 이해하기 (1) - 도입  (3) 2025.01.28
옵저버 패턴 정리  (0) 2024.07.21

인프런 널널한 개발자 강사님의 기초 탄탄! 독하게 시작하는 Java - Part2 : OOP와 JVM의 강의 중 JVM내용 정리


 

 

개발을 하고 서비스를 하면서 장애가 났을때에 다양한 원인이 있겠지만, 해결을 하려할 때 JVM에 대한 깊은 이해가 없을경우 해결을 못하는 경우가 있습니다. 

 

JVM에 대한 이해를 쌓고자 강의를 들으면서 알게 된 것들과 중요한 내용을 지금부터 정리해보겠습니다


 

 

JVM은 결과적으로 보면 프로세스이다.  java.exe 실행 파일을 실행시켜서 그 위에서 내가 만든 어플리케이션을 돌리는 것이다.

JVM은 기본적으로 Virtual Machine이다.

여담

널널한 개발자님 강의를 보다 보면 설명을 하시면서 물리적/논리적 개념으로 나누는데 

물리적인 부분 == H/W
논리적인 부분 == S/W
으로 봐도 된다.

또한 논리적이다 라는 말은 가상이라는 개념으로 봐도 된다고 종종 말씀하신다.
logical 을 IT쪽에서는 Virtual이라고 한다.
그리고 컴퓨터에서 CPU를 machine이라고도 한다고 하는데 그렇다면
Vircual Machine이다 라고 하면 CPU를 논리적으로 구현한것이다 라고 해석할 수 있다.

 

 

 

강사님께서 정말 많이 자주 그리시는 컴퓨터의 형태이다.

 

하드웨어 영역과 소프트웨어 영역으로 나누어져 있고, 소프트웨어영역은 Application영역과 System영역이 있다. System 영역의 가장 대표적인게 운영체제인데, 운영체제의 핵심적인 알맹이를 커널이라고 한다. 

커널은 그림에서 Kernel 영역에서 돌아가는 것이고

User Mode Application 영역이 있는데, 이것은 이제 위 그림에서 User 영역에서 돌아가는것이다.

 

우리가 소위 Native라고 부르는 영역은 H/W부터 Kernel까지 포함하는 구간이다.

그래서 그 부분을 개발하는것은 C/C++과 같이 Native언어로 개발을 하는데, C/C++코드가 CPU가 이해할 수 있는 언어로 직접 번역이 되어 실행합니다.

그렇기떄문에 H/W나 OS에 의존성이 있고, 의존성이 있다는 말은 Window에서 개발한 C/C++코드가 있으면 그 코드는 리눅스에서 제대로 동작하지 않을 수 있다는 것입니다.

 

처음에 말했듯 VM이라는 것은 컴퓨터를 소프트웨어적으로 구현한 것이다. 머신은 머신인데 이 머신이 인식할 수 있는 코드가 자바 바이트 코드라면 그걸 JVM 이라고 한다는 것이다.

 

결과적으로 JVM이라는것을 말하면 소프트웨어로 CPU도 구현돼있고, RAM도 구현돼있고, SSD도 구현돼있는 것이다. 따라서 JVM은 User Mode Applicatio 프로세스 임과 동시에 가상 메모리 공간을 RAM이나 보조기억장치처럼 활용 하면서 동시에 머신으로 연산도 해주는기능을 모두 포함하고 추가로 운영체제가 제공하는 기능 일부까지 다 들어있습니다.

 

따라서 JVM은 CS론에서 "OS/컴퓨터구조 를 전부 섞어서 C나 C++로 구현" 한게 JVM이 된다는 것이다.

그래서 위에서 잠깐 설명한 네이티브영역을 개발하는 언어들과 다르게 JVM만 있다면 내가 만든 자바코드는 JVM이 돌려주기 때문에 OS에 영향이 없이 JVM만 있다면 돌릴 수 있는 것이다.

 

C++ 과 Java의 메모리 관리 차이

C++ Java
- 객체에 대한 모든 관리 책임은 개발자에 있다.(소유권, 메모리 할당 및 해제)

- 객체의 생명주기에 모두 개입하는 구조
- 객체 메모리 해제는 전적으로 JVM몫이다.(개발자에게는 소유권도 책임도 없음)

- 문제 발생 시 구조를 알아야 대응이 가능함

 

C++은 자유도가 극상에 가깝다. 근데 자유도가 극상인 만큼 책임도 많이 져야한다.

자바는 GC가 메모리관리를 해준다. 

 


 

JVM 구성요소

출처 : https://medium.com/webeveloper/jvm-java-virtual-machine-architecture-94b914e93d86

 

1. Class Loader,  2. Runtime Data Area, 3.  Execution Engine으로 크게 3가지 영역으로 나뉜다.

아무 영역에 속하지 않은 Native Method이쪽은 User Mode가 아닌 Kernel모드 수준에서의 API를 호출하게 해주는걸 가능하게 해준다.

나는 3가지 영역에 대해서만 알아볼거고, 2번 Runtime Data Area영역이 가장 중요하다고 한다.

 

 

'CS > JAVA' 카테고리의 다른 글

JVM 이해하기 (2) - Class Loader  (2) 2025.01.29
옵저버 패턴 정리  (0) 2024.07.21

Work Queues : Competing Consumers Pattern 이란?

메시지를 여러 Consumer 에게 분배하여 작업을 분산처리하는 구조.

작업 부하를 효율적으로 분산하고, 병렬 처리를 가능하게 만들어 처리 속도를 향상시킵니다.

- Round Ronin 방식과 Fair Dispatch 방식을 사용하여 메시지를 Consumer에게 분배

- Fair Dispatch 방식은 메시지 수동 확인 모드로 개발하고 메시지 처리 비중 설정등을 통해 조정 가능함

 

 

특징

1. 경쟁적 메시지 소비

 - 여러 Consumer가 동일한 메시지 큐에서 메시지를 가져가서 처리합니다.

 - 특정 메시지는 한 번에 하나의 Consumer에 의해 처리되므로 메시지 중복 처리를 방지합니다.

2. 작업 분산

 - 메시지가 여러 Consumer 간에 분배되어 병렬로 처리되므로 작업 부하를 효율적으로 분산합니다.

3. 확장성

 - Consumer를 추가하거나 제거함으로써 작업 처리 능력을 동적으로 확장하거나 축소합니다.

4. 내결함성

 - Consumer 중 하나가 실패하더라도 다른 Consumer가 작업을 이어받아 처리할 수 있어 시스템이 중단되지 않고 작동합니다.

 

 

구현

구현은 4개의 class를 통해 구현합니다.

 

 

1. 설정파일

package com.example.HelloMessageQueue.step2;

import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {
    public static final String QUEUE_NAME = "WorkQueue";

    @Bean
    public Queue queue(){
        /**
         * Queue 생성자로 name과 durable(Boolean) 받음
         * durable : 휘발성이냐 아니냐 (volatile, persistent)
         * false로 주면 휘발성 - volatile -> 서버가 종료되거나 시작될 때 큐의 메시지가 사라짐
         * true로 주면 영속성 - persistent
         */
        return new Queue(QUEUE_NAME, true);
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
        return new RabbitTemplate(connectionFactory);
    }

    @Bean
    public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory,
                                                    MessageListenerAdapter listenerAdapter){
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setQueueNames(QUEUE_NAME);
        container.setMessageListener(listenerAdapter);
        container.setAcknowledgeMode(AcknowledgeMode.AUTO);
        return container;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter(WorkQueueConsumer workQueueTask){
        return new MessageListenerAdapter(workQueueTask, "workQueueTask");
    }
}

 

전에 했던 메시지 송수신 코드랑 달라진게 거의 없습니다. 

 

이번에는 Queue를 만들때durable 을 true로 주어서 시스템이 중단되더라도 Queue에 메시지가 남아있도록 합니다.

그리고

container를 생성할때

container.setAcknowledgeMode(AcknowledgeMode.AUTO);

이 부분이 추가됐습니다. 사실 위 코드는 작성하지 않더라도 기본값이 AUTO이기 때문에 쓰나 안쓰나 똑같지만 명시적으로 추가합니다.

 

 

2. Consumer

package com.example.HelloMessageQueue.step2;

import org.springframework.stereotype.Component;

@Component
public class WorkQueueConsumer {
    public void workQueueTask(String message){
        String[] messageParts = message.split("\\|");
        String originMessage = messageParts[0];
        int duration = Integer.parseInt(messageParts[1]);

        System.out.println("# Received: " + originMessage + " (duration: " + duration + "ms)");

        try{
            int seconds = duration / 1000;
            for (int i = 0; i < seconds; i++) {
                Thread.sleep(1000);
                System.out.print(".");
            }
        }catch (InterruptedException e){
            Thread.currentThread().interrupt();
        }

        System.out.println("\n[X] Completed: " + originMessage);
    }
}

 

| 를 기준으로 앞에있는건 메시지, 뒤에있는 숫자는 duration으로 설정해서 duration만큼 시간을 두고 메시지를 처리하도록 했습니다.

시간차를 줌으로써 메시지를 받는것을 확인할 수 있도록 했습니다.

 

 

3. Producer

package com.example.HelloMessageQueue.step2;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

@Component
public class WorkQueueProducer {
    private final RabbitTemplate rabbitTemplate;

    public WorkQueueProducer(RabbitTemplate rabbitTemplate){
        this.rabbitTemplate = rabbitTemplate;
    }

    public void sendWorkQueue(String workQueueMessage, int duration){
        String message = workQueueMessage + "| " + duration;
        rabbitTemplate.convertAndSend(RabbitMQConfig.QUEUE_NAME, message);
        System.out.println("Sent WorkQueue " + message);
    }
}

message를 | 로 이어서 통으로 보내게 합니다.

 

 

4. Controller

package com.example.HelloMessageQueue.step2;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class WorkQueueController {

    private final WorkQueueProducer workQueueProducer;

    public WorkQueueController(WorkQueueProducer workQueueProducer){
        this.workQueueProducer = workQueueProducer;
    }

    @PostMapping("/workqueue")
    public String workQueue(@RequestParam(name = "message") String message, @RequestParam(name = "duration") int duration){
        workQueueProducer.sendWorkQueue(message, duration);
        return "Work queue sent = " + message + ", (" + duration + ")";
    }
}



이렇게 해주고 서버를 시작해줍니다.

그리고 나서

curl -X "POST" "http://localhost:8081/api/workqueue?message=Task1&duration=2000"
curl -X "POST" "http://localhost:8081/api/workqueue?message=Task2&duration=4000"
curl -X "POST" "http://localhost:8081/api/workqueue?message=Task3&duration=5000"

 

명령프롬프트에서 위 명령어를 이용해 메시지를 쏴주면?

에러가 납니다.

한 번 RabbitMQ Management에 들어가보죵

 

 

 

 

Ready상태에 2건이 있는걸 볼 수 있는데 이건 Queue에 들어가긴 했는데, Consumer가 어떤 이유때문에 처리를 못 한 경우입니다.  큐를 생성할때 durable을 true로 설정했기 때문에 다시 서버를 시작하더라도 Queue에는 처리되지 않은 메시지들이 남아있을거에요. 그래서 처리될 때까지 기다릴 겁니다.

그래서 에러가 나면 빠르게 프로그램을 수정해서 재처리가 되게 하거나, Purge를 통해 메시지를 날려버려야 합니다..

 

에러가 난 이유를 보면

 

 

NumberFormatException이 났습니다. 

Producer가 메시지를 만들 때

public void sendWorkQueue(String workQueueMessage, int duration){
    String message = workQueueMessage + "| " + duration;
    rabbitTemplate.convertAndSend(RabbitMQConfig.QUEUE_NAME, message);
    System.out.println("Sent WorkQueue " + message);
}

 

"| " 이런식으로 | 뒤에 공백을 하나 줬고,

 

Consmer가 처리할 때

int duration = Integer.parseInt(messageParts[1]);

 

냅다 int형으로 바꾸려고 했기 때문에 처리를 하지 못한 것입니다.

int duration = Integer.parseInt(messageParts[1].trim());

으로 바꿔주고 서버를 재시작하면 서버를 켜자마자 메시지를 소비할거에요.

 

위처럼 바꾸고 서버를 켜보면?

 

이렇게 서버를 켜자마자 쌓여있던 2개의 메시지를 처리하고

래빗엠큐 매니지 페이지를 가보면

 

이렇게 메시지가 처리되어 0개로 나타게 되쥬

 

Ready 상태가 많아지게 되면 Consumer 수를 늘리거나 Consumer의 메시지 처리 속도를 튜닝하거나 해야한다고 합니다.

unacked 상태가 많은 경우에는 Consumer가 메시지를 처리를 못하는 것이기 때문에 빠르게 디버깅을 해서 ack를 보내거나, consumer 연결상태 확인, restart, 재연결, 프로세스 재시작 이런것들을 해야한다고 합니다.

 

 

라운드 로빈으로 처리되는지 확인해보기

 

인텔리제이 터미널에서 아래 명령어를 통해 빌드를 해줍니다.

 ./gradlew clean build

 

 

빌드가 완료되면

저 경로에 jar파일이 생성될거에요.

그러고 나서 저는 windows 환경이니까

cmd 창을 지금부터 3개를 띄울겁니다. 

 

1, 2번창에서 각각 포트번호를 다르게 하여 서버를 띄울 거고 3번 창에서는 요청을 날릴거에요.

우리가 라운드 로빈 형태로 동작하길 바라니까 

 

메시지 A,B,C를 보낸다고 치면, 요청을 처리하는 형태가 

첫 번째 서버가 A, C를 처리하고 두 번째 서버가 B를 처리하는 형태가 되어야 합니다.

이걸 눈으로 확인하기 위해 각각 서버를 실행해보겠습니다.

먼저 위 사진에서 jar파일이 있는 경로까지 들어가줍니다.

그러고 난 뒤에 아래 명령어를 통해 각각 다른 터미널에서 서버를 실행해줍니다.

java -jar HelloMessageQueue-0.0.1-SNAPSHOT.jar --server.port=8081
java -jar HelloMessageQueue-0.0.1-SNAPSHOT.jar --server.port=8082

 

저는 8080을 오라클이 쓰고있어서 8081이랑 8082로 했습니다.

 

그러면 각각 위 사진처럼 포트번호만 다른채로 실행이 될거에요

이제 또 다른 프롬프트(터미널)에서 curl 요청을 보내보겠습니다.

 

 

왼쪽 위(8081) 왼쪽아래(8082) 오른쪽(요청보내는곳)

상태에서

curl -X "POST" "http://localhost:8081/api/workqueue?message=Task1&duration=2000"
curl -X "POST" "http://localhost:8081/api/workqueue?message=Task2&duration=4000"
curl -X "POST" "http://localhost:8081/api/workqueue?message=Task3&duration=5000"

요청을 보내면??

 

위(8081) 에서 2번째 메시지를 처리하고, 아래(8082)에서 1, 3번쨰 요청을 가져가서 처리했네요

여기서 또 한번 날려보면??

이번에는 8081쪽에서 먼저 1, 3 번을 처리하고, 8082에서 2번을 처리했네요

이렇게 번갈아가면서 라운드로빈으로 처리하는걸 확인할 수 있었습니다.

관리페이지에서도 호출시점에 잘 처리된 걸 볼 수 있습니다.

 

 

이것으로 Work Queues 방식에 대해 알아보았습니다.

 

코드 출처 : https://github.com/villainscode/HelloMessageQueue/tree/tutorial-step2  

내용 : 인프런 코드빌런 강사님의  "RabbitMQ를 이용한 비동기 아키텍처 한방에 해결하기" 강의 내용 정리

 

git config --global user.email

git config --global user.name

이런거 다 설정 돼있다고 가정

프로젝트 있는 디렉토리에서 termianl / git bash로 접속했다고 가정

 

1. 레포 만들때 README.md 생성한 경우

git init
git remote -v // 원격 url 확인
git remote remove origin // 있을 경우 모든 원격 url 삭제
git remote add origin {레포주소}
git remote -v
git add .
git commit -m "init"
git pull origin main --rebase 
git push origin main

 

 

2. README.md 없이 생성한 경우

git init
git remote add origin {레포주소}
git add .
git commit -n "init"
git push -u origin main

 

'매번 찾기 귀찮은 것들' 카테고리의 다른 글

Docker로 mysql 설치/세팅/접속 (CLI)  (0) 2025.01.25

 

Window(cmd) / MAC(terminal) 에서 진행한다고 가정

 

1. mysql 이미지 다운로드하기

docker pull mysql:8.0.38

버전은 알잘딱

 

2. 이미지 조회

docker images

 

 

3. MySQL 환경 세팅

myproject 라는 컨테이너에서 root 계정의 비밀번호를 root라고 하고 3306포트 통해서 MySQL 띄우기

docker run --name mycontainer -e MYSQL_ROOT_PASSWORD=root -d -p 3306:3306 mysql:8.0.38

 

run : mysql 이미지를 컨테이너로 실행하는 명령어

--name {컨테이너이름} : 컨테이너의 이름 지정

MYSQL_ROOT_PASSWORD : MySQL 비밀번호 환경 변수 root로지정

-d : 컨테이너를 백그라운드에서 실행

-p 3306: 3306  : 호스트 포트와 mysql 컨테이너 포트 3306으로 매핑

  -> 3306 = mysql 서버가 사용하는 포트

  -> 호스트의 3306 포트로 컨테이너의 3306 포트로 접근

 

4. 실행중인 컨테이너 확인

docker ps

마지막에 -a 붙이면 (종료된거 포함)모든 컨테이너 확인

 

5. 컨테이너 접속

docker exec -it myproject bash

 

하고 나서 db접속은

 

mysql -u root -p

 

6. 컨테이너 종료됐으면

docker ps -a 로 컨테이너 ID확인
docker start {컨테이너ID}

 

 

7. 컨테이너 삭제

docker rm -f {컨테이너ID}

 

 


추가. 특정포트 물고있는 프로세스 죽이기

Window

netstat -ano | findstr 3306  # 으로 찾기
taskkill /pid {프로세스아이디} /f

 

MAC

lsof -i :{포트번호}  # 로 찾고
kill -15 {PID번호}  # 로 죽이기

 

kill -9 로도 종료할 수 있는데 이건 강제종료라고 한다(정보 유실 가능성)

-15로 죽이면 소프트웨어 종료 시그널이라서 더 안전하다고 한다.

15로도 안죽으면 9로 죽이랜다.

'매번 찾기 귀찮은 것들' 카테고리의 다른 글

깃허브 레포파고 프로젝트 올리기  (0) 2025.01.25

RabbitMQ를 이용해 초간단 메시지 전송예제를 따라해봤습니다.

 

순서

1. rabbitmq 서버 실행

2. 유저 및 VirtualHost 추가

3. 프로젝트생성

4. application.yml 작성

5. RabbitMQConfig.java 작성

6. Message Sender 구현

7. Message Receiver 구현

8. 메시지 전송 테스트

 


 

 

 

환경

OS : Window

IDE : Intellij Community

JDK : 17

 

1. 서버 실행

 

윈도우에서 RabbitMQ를 설치하고 환경변수까지 설정하면 RabbitMQ가 백그라운드에서 실행되고 있을거에요.

윈도우키 눌러서 rabbitmq 검색해서 나오는 저 앱들을 통해 백그라운드 실행을 관리할 수 있습니다.

저는 일단 stop으로 백그라운드에서 실행되는걸 멈췄고,

 

cmd에서 rabbitmq-server를 입력해주면

 

#으로 나름 토끼모양...을 그리면서 실행됩니다.

 

http://localhost:15672/

 

실행하고 난 뒤에 위 주소로 들어가진다면 성공.

 

2. 유저 및 VHost 추가

저는 기본계정을 삭제하고 admin이라는 새로운 계정을 만들었는데,

 

Add a user를 통해 테스트용 계정을 하나 더 생성했습니다.

그리고 처음 생성하면 Can access virtual hosts에 아래처럼 나올텐데

Name에서 직접 계정누르고 들어가서

 

Set permissions 버튼 누르면

이제 virtual host에 접근할 수 있습니다.

 

3. 프로젝트 생성

 

이제 https://start.spring.io/ 에 들어가서

 

Dependency 를 web, devtool, RabbitMQ 세 가지만 추가하고 프로젝트를 생성해줍니다.

적당히 서버 실행 잘 되는지 확인하고

 

4. application.yml 작성

 

application.yml을 아래와 같이 작성해줍니다.

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guestuser
    password: guestuser
  application:
    name: HelloMessageQueue
server:
  port: 8080

 

application name이랑 server port는 명시적으로 해줍니다

 

이제 기본 환경 세팅 끝!

 

5. RabbitMQConfig.java 작성

 

이제 RabbitMQConfig 파일을 작성해줄겁니다.

RabbitMQConfig  에서Bean을 4개 만들어줄건데,


- Queue queue() : Queue 인스턴스를 생성하고, Application이 사용할 큐를 정의하고 메시지를 전달하고 처리하는 기본 큐 세팅입니다. 

 

- RabbitTemplate rabbitTemplate(ConnectionFactory ConnectionFactory) : RabbitMQ와 통신하기 위한 템플릿 인스턴스를 생성하는 역할입니다. 메시지 송수신용

 

- SimpleMessageListenerContainer container(ConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter)

 

- MessageListenerAdapter listenerAdapter(Receiver receiver) : 수신한 메시지를 특정 클래스의 특정 메서드로 전달하는 어댑터, 인자로 전달된 메서드를 자동으로 호출합니다

 

위 의 Bean을 모두 구현한 코드는 아래와 같습니다.

package com.example.HelloMessageQueue.step1;

import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {
    
    public static final String QUEUE_NAME = "helloqueue";

    @Bean
    public Queue queue(){
        /**
         * Queue 생성자로 name과 durable(Boolean) 받음
         * durable : 휘발성이냐 아니냐 (volatile, persistent)
         * false로 주면 휘발성 - volatile -> 서버가 종료되거나 시작될 때 큐의 메시지가 사라짐
         * true로 주면 영속성 - persistent
         */
        return new Queue(QUEUE_NAME, false);
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
        return new RabbitTemplate(connectionFactory);
    }

    @Bean
    public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory,
                                                    MessageListenerAdapter listenerAdapter){
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setQueueNames(QUEUE_NAME);
        container.setMessageListener(listenerAdapter);
        return container;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter(Receiver receiver){
        return new MessageListenerAdapter(receiver, "receiveMessage");
    }
}

 

이제 각 Bean을 하나씩 살펴보면

 

 

@Bean
public Queue queue(){
    /**
     * Queue 생성자로 name과 durable(Boolean) 받음
     * durable : 휘발성이냐 아니냐 (volatile, persistent)
     * false로 주면 휘발성 - volatile -> 서버가 종료되거나 시작될 때 큐의 메시지가 사라짐
     * true로 주면 영속성 - persistent
     */
    return new Queue(QUEUE_NAME, false);
}

 

생성자의 인자로 두 가지를 받는데 첫 번째는 QUEUE_NAME, 다음은 boolean타입의 duration 입니다.큐네임은 말그대로 메시지가 쌓이고 처리될 큐의 이름을 지정하는 것이고duration은 큐가 휘발성인지, 영속성인지 여부를 정하는 옵션입니다.false로 설정하면 서버가 종료되거나 재시작 할 때 큐의 메시지가 사라집니다.

 

 

@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
    return new RabbitTemplate(connectionFactory);
}

 

JDBCTemplate과 비슷하게 RabbitMQ와 상호작용하기 위한 간단한 API를 제공합니다.
주로 메시지 전송을 담당합니다.
ConnectionFactory는 RabbitMQ와의 연결을 관리하는 객체로 rabbitTemplate에 주입하여 메시지를 전송할 때 사용할 Connection을 제공합니다.
메시지를 전송하는 Sender가 rabbitTemplate.convertAndSend() 메서드를 사용해 큐에 메시지를 넣는데 사용합니다

 

@Bean
public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory,
                                                MessageListenerAdapter listenerAdapter){
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames(QUEUE_NAME);
    container.setMessageListener(listenerAdapter);
    return container;
}

 

RabbitMQ 메시지를 비동기적으로 수신하기 위해 SimpleMessageListenerContainer 를 생성, 이 컨테이너가 특정 큐를 지속적으로 모니터링하고 메시지를 수신하면 지정된 리스너(MessageListenerAdapter)를 통 해 처리합니다.
ConnectionFactory는 RabbitMQ와 연결을 유지하며, 수신하는 메시지를 이 연결 을 통해 가져옵니다.
setQueueNames(QUEUE_NAME) 메서드는 특정 큐 이름을 설정. 이 컨테이너는 코드에서 설명한 큐 네임인 helloQueue에서 수신되는 메시지를 모니터링합니다.
setMessageListener(listenerAdapter)는 listenerAdapter를 설정하여, 메시지가 수신될 때 호출할 리스너를 지정합니다.

 

@Bean
public MessageListenerAdapter listenerAdapter(Receiver receiver){
    return new MessageListenerAdapter(receiver, "receiveMessage");
}

 

receiver 객체는 메시지를 처리하는 역할을 하는 Bean이며, receiveMessage 메서드 를 호출합니다.
MessageListenerAdapter는 RabbitMQ에서 수신된 메시지를 특정 메서드에 전달 할 수 있도록 해줍니다
이 경우, receiveMessage 메서드가 자동으로 호출되며, 메시지 내용을 인자로 받습니다
Receiver 클래스의 receiveMessage 메서드가 메시지를 수신하여 처리할 수 있도 록 설정합니다. (RabbitMQ에서 수신된 메시지가 receiver.receiveMessage(String message) 메서드로 전달합니다.)

 

6. Sender 구현

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

@Component
public class Sender { // 메시지가 전달이 됐을때 RabbitTemplate을 통해 큐에 전달하는

    private final RabbitTemplate rabbitTemplate;

    public Sender(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    public void send(String message){
        rabbitTemplate.convertAndSend(RabbitMQConfig.QUEUE_NAME, message);
        System.out.println("[#] Sent : " + message);
    }
}

 

RabbitTemplate을 주입받고 메서드를 사용합니다.

 

7. Receiver 구현

import org.springframework.stereotype.Component;

@Component
public class Receiver { // Consumer 역할
    public void receiveMessage(String message){
        System.out.println("[#] Received : " + message);
    }
}

 

Receiver는 Consumer 역할을 하고, 메서드 이름을 Config에서

@Bean
public MessageListenerAdapter listenerAdapter(Receiver receiver){
    return new MessageListenerAdapter(receiver, "receiveMessage");
}

어댑터에 적어준 이름과 같아야 합니다.

 

8. 테스트

적당하게 Controller 하나 만들어 줍니다.

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/message")
public class MessageController {
    private final Sender sender;

    public MessageController(Sender sender) {
        this.sender = sender;
    }

    @PostMapping("/send")
    public String sendMessage(@RequestBody String message){
        sender.send(message);
        return "[#] Message sent successfully " + message;
    }
}

 

 

 

 

저는 postman을 이용해서 테스트 햇씁니다.

 

 

 

 

이렇게 날려보면?

 

 

이런 응답을 받았고 콘솔을 확인해보면?

잘 주고받은것 같습니다 이제 rabbitmq 모니터링 페이지에 가서 Queues and Streams 탭에 가보면

 

코드에서 설정한 QUEUE_NAME의 큐 이름으로 큐가 생겨있고 눌러서 들어가보면 위 화면처럼 트래픽을 확인할 수 있고

이것저것 모니터링 할 수 있습니다.

 

이렇게 간단하게 메시지만 주고받는 예제를 해봤습니다. 끝

 

+ Recent posts