IT 핵심 개념 정리 시리즈
- 백엔드 용어 정리 (현재 글)
- 네트워크 용어 정리
이 글의 바로가기
용어정리 – 데이터베이스
트랜잭션
-
트랜잭션 : 하나의 논리적 기능을 수행하기 위한 작업 단위다. 여러 SQL이 함께 실행되더라도 하나의 작업처럼 다루며,
모두 성공하면 커밋하고 중간에 실패하면 롤백해 데이터 정합성을 지킨다. - ACID 속성 : 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질
- Atomicity(원자성): 트랜잭션의 연산은 모든 연산이 완벽히 수행되어야 하며, 한 연산이라도 실패하면 트랜잭션 내의 모든 연산은 실패한다.
- Consistency(일관성): 트랜잭션은 제약 조건, 외래 키, 비즈니스 규칙을 만족하는 유효한 상태에서 또 다른 유효한 상태로만 변경되어야 한다.
- Isolation(고립성): 트랜잭션은 동시에 실행될 경우 다른 트랜잭션에 의해 영향을 받지 않고 독립적으로 실행되어야 한다.
- Durability(내구성): 트랜잭션이 커밋된 이후에는 시스템 오류가 발생하더라도 커밋된 상태로 유지되는 것을 보장해야 한다.
- 트랜잭션 격리수준(Isolation Level): 동시에 실행되는 트랜잭션을 서로 얼마나 고립시킬지 정하는 기준이다.
격리 수준이 높을수록 정합성은 좋아지지만 락 경합과 대기 시간이 늘 수 있고, 낮을수록 성능은 유리하지만
Dirty Read, Non-Repeatable Read, Phantom Read 같은 이상 현상이 생길 수 있다.- SERIALIZABLE: 가장 강력한 격리수준으로, 동시에 실행한 결과가 순서대로 하나씩 실행한 것과 같도록 보장한다. DBMS에 따라 잠금, predicate lock, 순차 실행 보장 실패 검출 등 구현 방식이 다르며 동시처리 성능이 낮아질 수 있다.
- REPEATABLE READ: 같은 트랜잭션에서 같은 행을 다시 읽을 때 일관된 결과를 보장하는 격리 수준이다. 스냅샷 생성 시점과 Phantom Read 처리 방식은 DBMS마다 다르며, MySQL InnoDB는 일반 읽기에 MVCC를, 잠금 읽기에 Next-Key Lock을 활용한다.
- READ COMMITTED: 어떤 트랜잭션의 변경내용이 COMMIT되어야만 다른 트랜잭션에서 조회할 수 있다. Non-Repeatable Read가 발생한다.
- READ UNCOMMITTED: 어떤 트랜잭션의 변경내용이 COMMIT이나 ROLLBACK에 상관없이 모두 노출된다. Dirty Read가 발생할 수 있다.
- 트랜잭션 부정합 종류:
- Phantom Read(유령 읽기) : 같은 트랜잭션에서 동일한 범위 조건으로 다시 조회했을 때, 다른 트랜잭션의 삽입·삭제로 결과 행 집합이 달라지는 현상
- Non-Repeatable Read(반복 읽기 불가능) : 같은 트랜잭션에서 동일한 행을 다시 읽었을 때, 다른 트랜잭션이 커밋한 수정·삭제로 결과가 달라지는 현상
- Dirty Read : 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽는 현상. 해당 트랜잭션이 롤백하면 실제로 확정되지 않은 값을 읽은 셈이 된다.
-
격리 수준과 이상 현상 정리
격리 수준 Dirty Read Non-Repeatable Read Phantom Read READ UNCOMMITTED 발생 가능 발생 가능 발생 가능 READ COMMITTED 방지 발생 가능 발생 가능 REPEATABLE READ 방지 방지 DBMS 구현에 따라 다름 SERIALIZABLE 방지 방지 방지 MySQL InnoDB의
REPEATABLE READ는 일반 읽기에서는 MVCC 스냅샷을, 잠금 읽기에서는 Next-Key Lock을 활용해 Phantom Read를 제어한다. 모든 DBMS의REPEATABLE READ가 같은 방식으로 동작하는 것은 아니다.
락과 MVCC
- 락(Lock) : 동시 접근으로부터 데이터 정합성을 보호하기 위한 잠금 메커니즘
- 공유 락(Shared Lock, S Lock, 읽기 락) : 여러 트랜잭션이 동시에 읽을 수 있으나, 쓰기는 막는다. 공유 락끼리는 호환된다.
- 배타 락(Exclusive Lock, X Lock, 쓰기 락) : 한 트랜잭션이 독점하며 다른 트랜잭션의 충돌하는 락 획득과 쓰기를 막는다. 일반 조회까지 막는지는 MVCC 사용 여부와 조회 방식 등 DBMS 구현에 따라 다르다.
- 비관적 락(Pessimistic Lock) : 충돌이 자주 발생한다고 가정하고,
데이터를 읽는 시점에 미리 DB 락(SELECT ... FOR UPDATE)을 건다. 충돌이 잦은 환경에 적합하나 동시성이 떨어지고 데드락 위험이 있다. - 낙관적 락(Optimistic Lock) : 충돌이 드물다고 가정하고 락을 걸지 않으며,
보통 버전(version) 컬럼으로 갱신 시점에 충돌 여부를 검사한다. 변경 전후 버전이 다르면 예외를 던져 재시도한다. 동시성은 좋으나 충돌이 잦으면 재시도 비용이 크다.
-
MVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어) : 데이터를 변경할 때 이전 버전(스냅샷)을 함께 보관해, 읽기 작업이 쓰기 락을 기다리지 않도록 하는 동시성 제어 기법.
읽는 트랜잭션은 자신의 시점에 맞는 스냅샷을 보고, 쓰는 트랜잭션은 새로운 버전을 만든다. 이로써 읽기-쓰기 간 블로킹을 줄여 동시성을 높인다. (MySQL InnoDB, PostgreSQL, Oracle 등이 사용) - 락 선택 기준 : 충돌이 많고 반드시 직렬화가 필요하면 DB의 비관적 락(
SELECT ... FOR UPDATE)이나 원자적 조건 업데이트가 단순하고 안전하다. 충돌이 적으면 버전 컬럼 기반 낙관적 락으로 재시도한다. Redis 분산 락도 사용할 수 있지만 락 만료, 락 소유자 검증, 네트워크 장애를 함께 고려해야 하므로 강한 정합성이 필요하면 DB 락을 우선 검토한다.
인덱스
-
인덱스(Index) : 테이블 검색 속도를 높이기 위한 자료구조다. 조건에 맞는 데이터를 찾을 때 전체 테이블을 스캔하지 않고 정렬된 구조를 탐색하므로 조회 성능을 높일 수 있지만, 쓰기 시 인덱스 갱신 비용과 저장 공간이 추가로 든다.
- B-Tree / B+Tree 인덱스 구조 : 대부분의 RDBMS 인덱스는 B+Tree 기반이다.
루트에서 리프까지의 깊이가 일정한 균형 트리라 일반적으로 O(log n) 수준의 검색 성능을 내며, 리프 노드가 정렬되어 있어 범위 검색과 정렬에도 유리하다.- 리프 노드 연결 구조 : B+Tree는 검색 키와 행을 찾기 위한 정보(행 위치 또는 PK 등)를 리프 노드에 저장하고, 리프 노드를 순차 탐색할 수 있도록 서로 연결한다. 세부 저장 방식은 DBMS와 인덱스 종류에 따라 다르다. 덕분에 한 지점을 찾은 뒤 옆으로 순차 이동하며 읽을 수 있어 범위 검색(BETWEEN, 부등호)과 정렬(ORDER BY)에 매우 유리하다.
- 왜 빠른가 : 정렬된 상태로 유지되므로 이진 탐색처럼 범위를 좁혀가며 찾고, 디스크 I/O 횟수(트리 깊이)가 적기 때문이다.
- 인덱스가 동작하지 않는 경우 / 주의점 : 인덱스를 걸어도 옵티마이저가 사용하지 않거나 효율이 떨어지는 대표 케이스
- 선두 컬럼 미사용 : 복합 인덱스 (A, B)에서 선두 컬럼 A가 조건절에 없으면 인덱스를 제대로 타기 어렵다.
- 인덱스 컬럼에 함수/연산 사용 :
WHERE SUBSTR(col,1,3)='abc',WHERE col+1 = 10처럼 컬럼을 가공하면 인덱스를 타지 못한다. (함수 기반 인덱스를 따로 만들면 가능) - LIKE ‘%xx’ : 일반적인 B-Tree 인덱스는 검색 시작점을 정할 수 없어 선두 와일드카드 검색에 효율적으로 사용하기 어렵다.
LIKE 'xx%'같은 전방 일치는 문자 정렬 규칙과 DBMS 조건이 맞으면 범위 검색에 인덱스를 활용할 수 있다. - 형변환(묵시적 타입 캐스팅) : 컬럼 타입과 조건 값 타입이 달라 묵시적 형변환이 일어나면 인덱스를 타지 못할 수 있다.
- 카디널리티가 낮은 컬럼 : 성별처럼 값의 종류가 적으면 인덱스 효과가 작다. NULL이 많은 컬럼이나 부정 조건(
!=,NOT IN)도 비효율적이다.
- 복합 인덱스와 커버링 인덱스
- 복합 인덱스(Composite Index) : 여러 컬럼을 묶어 만든 인덱스. 컬럼 순서가 중요하며, 선두 컬럼부터 차례로 조건에 사용되어야 효율적이다.
등치(=) 조건 컬럼을 앞에, 범위 조건 컬럼을 뒤에 두는 것이 유리하다. - 커버링 인덱스(Covering Index) : 쿼리가 필요로 하는 컬럼을 인덱스가 모두 포함하는 경우.
실제 테이블(데이터 블록) 접근 없이 인덱스만 읽어 결과를 반환하므로 매우 빠르다.
- 복합 인덱스(Composite Index) : 여러 컬럼을 묶어 만든 인덱스. 컬럼 순서가 중요하며, 선두 컬럼부터 차례로 조건에 사용되어야 효율적이다.
실행 계획, 조인, 페이징
- 옵티마이저와 실행계획(Execution Plan) : 옵티마이저는 SQL을 어떤 순서와 방식으로 처리할지 비용을 계산해 실행 계획을 선택하는 DBMS 엔진이다.
실행 계획을 보면 인덱스 사용 여부, 조인 방식, 예상 row 수, 풀 스캔 여부를 확인할 수 있어 쿼리 튜닝의 출발점이 된다.- 실행계획(Execution Plan) : 옵티마이저가 결정한 SQL 처리 경로. 인덱스 사용 여부, 조인 방식, 예상 처리 행 수 등을 보여준다.
- EXPLAIN : 실행계획을 미리 확인하는 명령. (MySQL:
EXPLAIN,EXPLAIN ANALYZE) 쿼리 튜닝 시 인덱스를 타는지,
풀 스캔을 하는지, 예상 row 수가 얼마인지를 확인하는 1차 도구다.
- JOIN 기법 종류
- 중첩 루프 조인 (Nested Loops Join) : 선행 테이블의 결과를 하나씩 읽고 후행 테이블을 반복 조회하는 방식. 선행 테이블의 결과가 작고,
후행 테이블의 조인 컬럼에 인덱스가 있을 때 유리하다. 선행 테이블의 처리 범위가 전체 비용을 크게 좌우한다. - 병합 조인 (Sort Merge Join) : 양쪽 입력을 조인 키 기준으로 정렬한 뒤 순차적으로 비교하는 방식.
이미 정렬되어 있거나 정렬 비용이 감당 가능할 때 유리하며, 동등 조건뿐 아니라 범위 조건 조인에도 사용할 수 있다. - 해시 조인 (Hash Match Join) : 작은 입력으로 해시 테이블을 만들고 큰 입력을 탐색하면서 매칭하는 방식.
대량 데이터의 동등 조인(=)에 유리하지만, 해시 테이블을 만들 메모리가 부족하면 디스크 I/O가 늘어날 수 있다.- Build Input (outer table) : 테이블 중 작은 테이블을 읽어 해시 테이블을 생성한다.
- Probe Input (inner table) : 큰 테이블을 읽으며 해시 테이블을 탐색하며 Join 한다.
- 중첩 루프 조인 (Nested Loops Join) : 선행 테이블의 결과를 하나씩 읽고 후행 테이블을 반복 조회하는 방식. 선행 테이블의 결과가 작고,
- 페이징 처리 : 대량 데이터를 나눠 조회하는 방법
- OFFSET 기반 페이징 :
LIMIT n OFFSET m방식. 구현이 간단하지만 **뒤 페이지로 갈수록 느려진다.
** OFFSET이 커지면 DB가 건너뛸 행까지 모두 읽고 버려야 하기 때문이다. 또한 페이징 중 데이터가 추가/삭제되면 행이 밀려 중복/누락이 발생할 수 있다. - 커서 기반 페이징(Cursor / Keyset Pagination) : 마지막 키로 다음 데이터를 조회한다. 예:
WHERE id < lastId ORDER BY id DESC LIMIT n인덱스를 그대로 타므로 깊은 페이지에서도 성능이 일정하다. 연속 조회에는 적합하지만 임의 페이지 이동은 어렵다.
- OFFSET 기반 페이징 :
- 페이징 방식 선택 기준 : 특정 페이지로 바로 이동해야 하면 OFFSET 방식을 유지하되 검색 조건과 인덱스를 함께 튜닝한다. 깊은 페이지 조회가 잦고 데이터 변경이 많은 목록은 커서 기반 페이징이 유리하다. 커서 방식은 앞의 행을 버리는 비용이 없고 데이터 추가/삭제 중 중복이나 누락 가능성도 줄일 수 있다.
인덱스 운영과 저장 구조
-
클러스터링(Clustering) : 연관된 데이터를 물리적으로 가깝게 배치해 디스크 I/O를 줄이는 기법이다. 구현 방식은 DBMS마다 다르며, 대표적으로 클러스터 인덱스는 인덱스 키 순서에 따라 테이블 데이터를 저장한다. 조회에는 유리할 수 있지만 쓰기·페이지 분할 비용을 함께 고려해야 한다.
- Clustered Index와 Non-Clustered Index
- 클러스터 인덱스(Clustered Index): 테이블 데이터가 인덱스 키 순서에 가깝게 저장되는 구조. 테이블당 하나만 가능하다.
MySQL InnoDB에서는 Primary Key가 클러스터 인덱스가 되며, PK가 없으면 내부적으로 대체 키를 사용한다. - 논클러스터 인덱스(Non-Clustered Index, Secondary Index): 데이터 자체와 별도의 인덱스 구조를 가진다.
인덱스에서 원하는 키를 찾은 뒤 실제 행을 다시 찾아가야 할 수 있다. InnoDB의 보조 인덱스는 리프 노드에 Primary Key 값을 저장한다.
- 클러스터 인덱스(Clustered Index): 테이블 데이터가 인덱스 키 순서에 가깝게 저장되는 구조. 테이블당 하나만 가능하다.
- Index 설정 기준
- 카디널리티 (Cardinality) 높음 : 중복도가 낮은 컬럼
- 선택도 (Selectivity) 높음 : 조건에 맞는 행이 적게 조회되는 컬럼
- INDEX SCAN 종류
- INDEX RANGE SCAN : B*Tree인덱스의 가장 일반적이고 정상적인 형태, 필요한 범위만 스캔
- INDEX FULL SCAN : 처음부터 끝까지 수평적으로 탐색하는 방식, 최적의 인덱스가 없을 때 차선으로 선택
- INDEX UNIQUE SCAN : 수직적 탐색만으로 데이터를 찾는 방식, 등치(=)조건으로 탐색하는 경우에 작동
- INDEX SKIP SCAN : 인덱스 선두 컬럼이 조건절에 없어도 인덱스를 활용하는 방식,
조건절에 빠진 인덱스 선두 컬럼의 중복값이 많고 후행 컬럼의 중복값이 적을때 활용
-
인덱스 추가 후에도 느릴 때 점검 순서 :
EXPLAIN또는EXPLAIN ANALYZE로 실행 계획을 먼저 본다. 인덱스가 실제 사용되는지,
예상 row 수와 실제 row 수가 크게 다른지, 풀 스캔이나 filesort가 발생하는지 확인한다. 이후 조건절이 인덱스 선두 컬럼을 사용하는지, 컬럼에 함수/형변환이 걸려 있는지, 카디널리티가 낮아 옵티마이저가 인덱스를 포기했는지 점검하고, 필요하면 복합 인덱스 순서나 커버링 인덱스를 검토한다. -
Oracle SQL 튜닝 포인트 : Oracle에서도 실행계획 확인, 복합 인덱스 선두 컬럼, 카디널리티, 조인 순서, 정렬 비용을 먼저 본다. 날짜 컬럼에
TRUNC(date_col)처럼 함수를 걸면 일반 인덱스를 타기 어려울 수 있으므로date_col >= :from AND date_col < :to같은 범위 조건으로 바꾸거나 함수 기반 인덱스를 검토한다. 대량 조회는 온라인 조회와 배치 집계를 분리하고, 조회 조건과 정렬 기준에 맞춰 복합 인덱스를 설계한다. - 인덱스(Index) 유형
- Filtered Index : 비 클러스터형 인덱스를 만들 때 WHERE 절을 사용해 거의 선택되지 않는 데이터를 제외하고 인덱스를 생성하는 방법,
필터 인덱스는 전체 테이블 인덱스에 비해 쿼리 성능을 개선하고 인덱스 유지관리 비용과 인덱스 저장소 비용을 줄일 수 있다는 장점이 있다. (SQL Server에서 사용가능) - Unique Index : Unique Index로 컬럼이 잡혀 있으면 해당 컬럼에는 중복된 데이터가 들어가지 않는다. 단,
NULL허용 방식은 DBMS마다 다르다. MySQL과 PostgreSQL은 기본적으로 여러NULL을 허용하지만, SQL Server의 일반 Unique Index는 하나의NULL만 허용한다. Oracle은 모든 키 컬럼이NULL인 행을 B-Tree 인덱스에 저장하지 않는 등 구현 차이가 있다. Primary Key는 DBMS와 관계없이NULL을 허용하지 않는다.
- Filtered Index : 비 클러스터형 인덱스를 만들 때 WHERE 절을 사용해 거의 선택되지 않는 데이터를 제외하고 인덱스를 생성하는 방법,
인덱스 설계 검증 기준
- 복합 인덱스의 실제 정렬 : 복합 인덱스
(A, B, C, D)의 리프 노드는(A → B → C → D)사전식 순서로 정렬된다.A가 같은 행끼리B가 모인다.A,B가 같을 때만C,D의 순서가 의미를 가진다.A = ? AND B = ? AND C >= ?는 연속된 리프 범위를 읽을 수 있다.B = ?만으로는 일반적으로 연속 범위를 특정할 수 없다.
-
범위 조건 이후의 컬럼 :
(A, B, C)에서A = ? AND B >= ? AND C = ?라면,B의 범위가 시작된 이후C는 탐색 범위를 더 좁히는 키로 쓰기 어렵다. DBMS가C를 인덱스 조건으로 추가 평가할 수는 있지만,A,B가 모두 등치 조건인 경우만큼 효율적이지 않다. - 인덱스와 정렬의 관계
WHERE A = ? ORDER BY B, C는(A, B, C)인덱스로 정렬 작업을 피할 수 있다.- 정렬 방향이 섞이거나 중간 컬럼이 빠지면 별도 정렬(filesort)이 필요할 수 있다.
- 실행 계획에서
Using filesort, 실제 읽은 행 수, 정렬 메모리·디스크 사용을 확인한다.
- 쓰기 비용과 페이지 분할
- 인덱스 하나를 추가하면
INSERT,UPDATE,DELETE마다 해당 B+Tree도 갱신된다. - 삽입 위치의 페이지가 가득 차면 페이지 분할이 발생한다.
- 무작위 UUID는 분할 비용을 키울 수 있고, 단조 증가 키는 특정 페이지 경합을 키울 수 있다.
- 조회 성능뿐 아니라 쓰기 경로와 저장 공간을 함께 평가한다.
- 인덱스 하나를 추가하면
- 성능 검증 기록
- 같은 데이터 규모, 캐시 상태, 동시성, 쿼리 파라미터 분포에서 전후를 비교한다.
EXPLAIN ANALYZE, p50·p95·p99, TPS, CPU·디스크 I/O, 인덱스 크기를 기록한다.INSERT·UPDATE지연과 느린 쿼리 로그도 함께 확인한다.
데이터 무결성
- 데이터 무결성: 데이터의 정확성, 일관성, 유효성을 유지하는 것, RDBMS의 중요한 기능으로 주로 데이터에 적용하는 연산을 제한하여 무결성을 유지한다.
- 개체 무결성(Entity integrity): 모든 테이블이 기본키로 선택된 필드를 가지고 있어야 한다. 기본키로 선택된 필드는 NULL을 허용하지 않는다.
- 참조 무결성(Referential integrity): 참조관계에 있는 두 테이블의 데이터가 항상 일관된 값을 갖도록 유지하는 것
데이터 모델링과 DB 선택
- 정규화: 데이터 중복을 줄이고 무결성을 높이기 위해 테이블을 체계적으로 분리하는 과정. 중복 설계는 삽입/삭제/갱신 이상을 만들 수 있다.
- 제 1 정규형 (1NF: First Normal Form) : 각 필드는 하나의 값만 가져야 한다.
- 제 2 정규형 (2NF: Second Normal Form) : 1NF를 만족하고, 기본 키가 복합 키일 때 모든 비주요 속성이 기본 키 전체에 완전 함수 종속되어야 한다. 즉, 키 일부에만 종속되는 속성이 없어야 한다.
- 제 3 정규형 (3NF: Third Normal Form) : 2NF를 만족하고, 비주요 속성이 기본 키가 아닌 다른 비주요 속성에 이행적으로 종속되지 않아야 한다.
- 보이스-코드 정규형 (BCNF: Boyce-Codd Normal Form) : 모든 결정자가 후보 키(candidate key)이어야 한다.
- 제 4 정규형 (4NF: Fourth Normal Form) : 다치 종속성(Multi-Valued Dependency)을 제거
- 제 5 정규형 (5NF: Fifth Normal Form) : 조인 종속성(Join Dependency)을 제거
- SQL vs NoSQL : 데이터 모델과 일관성/확장 전략의 차이
- SQL(관계형 DB) : 정해진 스키마(테이블/행/열), ACID 트랜잭션, JOIN과 복잡한 쿼리에 강점. 복제·샤딩 등 수평 확장도 가능하다.
- NoSQL : 문서/키-값/컬럼/그래프 등 다양한 모델을 제공하며, 유연한 스키마와 대용량·고처리량에 강점이 있다. 일관성 모델과 트랜잭션 지원은 제품·설정마다 다르다. (MongoDB, Cassandra, Redis, DynamoDB 등)
- 선택 기준 : 데이터 구조가 명확하고 트랜잭션·정합성·복잡한 조인이 중요하면 SQL,
스키마가 자주 변하거나 초대용량·고확장·고가용성이 중요하면 NoSQL이 유리하다.
DB 확장과 운영
-
Replication(복제) : Primary의 데이터를 Replica로 복제해 읽기 부하 분산과 장애 대응에 활용한다.
보통 비동기 복제이므로 Primary 장애 직후 일부 데이터 유실이나 복제 지연(Replication Lag)을 고려해야 한다. -
Sharding(샤딩) : 데이터를 여러 DB 노드에 나누어 저장하는 수평 분할 방식. 대용량 쓰기/저장 한계를 넘는 데 유리하지만, 샤딩 키 선택, 조인 제약,
리밸런싱, 운영 복잡도가 증가한다. -
데이터베이스 파티셔닝: 큰 테이블을 파티션(Partition)이라는 작은 단위로 나누어 관리하는 기법이다. 파티션 제거(Partition Pruning)가 가능한 쿼리는 성능과 관리성을 높일 수 있지만, 파티션 키와 맞지 않는 조인·조회는 오히려 비용이 커질 수 있다.
-
DB Migration : 스키마 변경을 버전으로 관리하는 방식. 운영에서는 하위 호환 가능한 변경을 먼저 배포하고, 애플리케이션 배포 후 불필요한 컬럼/인덱스를 제거하는 식으로 단계적으로 진행하는 것이 안전하다. (Flyway, Liquibase)
용어정리 – JPA
영속성 컨텍스트와 엔티티 생명주기
- JPA / Hibernate / Spring Data JPA : JPA는 자바의 ORM 표준 명세(인터페이스), Hibernate는 JPA를 구현한 대표적인 구현체, Spring Data JPA는 JPA를 더 쉽게 쓰도록 Repository 인터페이스만 정의하면 구현체를 자동 생성해주는 스프링 모듈이다.
- ORM(Object-Relational Mapping) : 객체와 관계형 DB의 테이블을 매핑해, SQL을 직접 작성하지 않고 객체 중심으로 데이터를 다루게 해주는 기술. 객체-테이블 간 패러다임 불일치를 해결한다.
-
영속성 컨텍스트 : 엔티티를 관리하는 JPA의 1차 캐시 공간. 엔티티 조회, 변경 감지, 쓰기 지연, 동일성 보장을 제공한다.
- 영속성 컨텍스트의 이점
- 1차 캐시 : 조회가 가능하며 1차 캐시에 없으면 DB에서 조회하여 1차 캐시에 올려 놓는다.
- 동일성 보장 : 같은 트랜잭션/영속성 컨텍스트 안에서는 같은 식별자의 엔티티에 대해 동일성 비교(
==)가 가능하다. - 쓰기 지연 : 트랜잭션 커밋하기 전까지 SQL을 바로 보내지 않고 모아서 보낼 수 있다.
- 변경 감지 : commit 되는 시점에 Entity와 스냅샷을 비교하여 update SQL을 생성한다.
- 지연 로딩 : 연관 엔티티를 실제 사용하는 시점에 SQL을 실행하여 데이터를 가져온다.
-
변경 감지(Dirty Checking) : 영속 상태의 엔티티 값을 변경하면,
트랜잭션 커밋 시점에 영속성 컨텍스트가 최초 스냅샷과 비교해 변경된 부분에 대한 UPDATE SQL을 자동 생성/실행한다. 영속 상태에서는 별도의save()호출이 필요 없다. 단, 준영속(detached) 엔티티는 변경 감지가 동작하지 않는다. - 프록시(JPA) : 연관된 엔티티가 항상 필요한 것은 아니므로, 실제 사용될 때까지 DB 조회를 지연시키기 위해 실제 엔티티를 대신하는 가짜 객체를 사용한다.
이를 프록시 객체라고 한다.- LazyInitializationException : 영속성 컨텍스트가 닫힌 뒤 지연 로딩 프록시를 초기화하려고 하면 발생한다.
서비스 계층의 트랜잭션 안에서 필요한 데이터를 조회하거나 DTO로 변환해 해결한다. OSIV(Open Session In View)를 켜두면 View 계층에서도 지연 로딩이 가능하지만, DB 커넥션을 오래 잡을 수 있어 주의가 필요하다.
- LazyInitializationException : 영속성 컨텍스트가 닫힌 뒤 지연 로딩 프록시를 초기화하려고 하면 발생한다.
- 엔티티 생명주기(Entity Lifecycle)
- 비영속(Transient) : 영속성 컨텍스트와 무관하게 객체만 생성된 상태 (
new) - 영속(Managed) : 영속성 컨텍스트가 관리하는 상태 (
persist(), 조회). 변경 감지(Dirty Checking) 대상. - 준영속(Detached) : 영속 상태였다가 분리된 상태 (
detach(),clear(), 트랜잭션 종료). 변경 감지가 동작하지 않음. - 삭제(Removed) : 삭제 예정 상태 (
remove())
- 비영속(Transient) : 영속성 컨텍스트와 무관하게 객체만 생성된 상태 (
- merge와 persist 차이
- persist : 새 엔티티를 영속 상태로 만든다. 이미 영속 상태가 된 객체 자체가 관리 대상이다.
- merge : 준영속 엔티티의 값을 영속 엔티티에 복사하고, 복사된 영속 엔티티를 반환한다.
파라미터로 넘긴 준영속 객체가 다시 영속 상태가 되는 것이 아니므로 반환값을 사용해야 한다.
-
flush : 영속성 컨텍스트의 변경 내용을 DB에 반영(SQL 전송)하는 것. 영속성 컨텍스트를 비우는 것이 아니라 동기화하는 것. (커밋,
JPQL 쿼리 실행 시 자동 호출) - 영속성 전이(Cascade)와 고아 객체(orphanRemoval) : Cascade는 부모 상태 변화를 자식에게 전파한다. 예: 부모 저장 시 자식도 저장한다.
orphanRemoval은 부모와의 연관관계가 끊긴 자식 엔티티를 자동 삭제하는 옵션.
N+1과 조회 최적화
- 페치 전략(Fetch Type) : 연관된 엔티티를 언제 로딩할지 결정하는 전략
- 즉시 로딩(EAGER) : 엔티티 조회 시 연관 엔티티도 즉시 함께 조회. 예측이 어렵고 N+1을 유발하기 쉽다. (
@ManyToOne,
@OneToOne의 기본값) - 지연 로딩(LAZY) : 연관 엔티티를 실제 사용하는 시점에 조회(프록시).
실무에서는 모든 연관관계를 LAZY로 설정하고 필요 시 Fetch Join으로 해결하는 것을 권장. (@OneToMany,@ManyToMany의 기본값)
- 즉시 로딩(EAGER) : 엔티티 조회 시 연관 엔티티도 즉시 함께 조회. 예측이 어렵고 N+1을 유발하기 쉽다. (
- N + 1 문제 : 연관 관계가 설정된 엔티티를 조회할 경우 조회된 데이터 개수(N)만큼 연관관계 조회 쿼리가 추가로 발생하는 문제.
1개의 쿼리를 예상했지만 연관 엔티티 조회 때문에 N개의 쿼리가 더 실행된다.- 즉시 로딩에서 발생 : JPQL을 사용하는 경우 전체 조회를 했을 때,
영속성 컨텍스트가 아닌 데이터베이스에서 직접 데이터를 조회한 다음 즉시로딩 전략이 동작하기 때문. - 지연 로딩에서 발생 : 지연로딩 전략을 사용한 하위 엔티티를 로드할 때,
JPA에서 프록시 엔티티를 unproxy 할 때 해당 엔티티를 조회하기 위한 추가적인 쿼리가 실행되어 발생.
- 즉시 로딩에서 발생 : JPQL을 사용하는 경우 전체 조회를 했을 때,
- N + 1 해결
- 패치 조인(Fetch Join) : 미리 JOIN하여 한 번에 데이터를 가져오면 N+1을 방지할 수 있다.
- 1:N 컬렉션 Fetch Join은 row 증가와 엔티티 중복을 만들 수 있어
distinct가 필요할 수 있다. - 컬렉션 Fetch Join과 페이징을 함께 사용하면 메모리 페이징 문제가 발생할 수 있다.
- 둘 이상의 컬렉션을 동시에 Fetch Join하면
MultipleBagFetchException이 발생할 수 있다.
- 1:N 컬렉션 Fetch Join은 row 증가와 엔티티 중복을 만들 수 있어
- 배치 사이즈(Batch Size) :
@BatchSize또는default_batch_fetch_size를 설정한다.
한 번의 쿼리로 여러 연관 엔티티를 조회하는 방법이다. 엔티티를 지정한 개수 만큼 IN을 활용하여 호출하게 된다. - EntityGraph : 조회 시 필요한 연관관계를 그래프로 지정하여 Fetch Join과 비슷하게 한 번에 로딩할 수 있다.
Repository 메서드에 적용하기 쉽다.
- 패치 조인(Fetch Join) : 미리 JOIN하여 한 번에 데이터를 가져오면 N+1을 방지할 수 있다.
- 조회 최적화 판단 기준
- 한 화면에서 항상 필요한 연관 데이터는 Fetch Join이나 EntityGraph로 명시적으로 함께 조회한다.
- 1:N 컬렉션 조회와 페이징이 섞이면 Fetch Join보다 batch size나 DTO 직접 조회가 안전한 경우가 많다.
- 연관관계는 기본적으로 LAZY로 두고, 조회 목적마다 필요한 데이터만 가져온다.
- JPQL과 QueryDSL : 복잡한 조회 쿼리 작성 방법
- JPQL : 테이블이 아닌 엔티티 객체를 대상으로 하는 객체지향 쿼리. 문자열이라 컴파일 시점에 오류를 잡지 못한다.
- QueryDSL : 자바 코드로 타입 안전(Type-safe)하게 쿼리를 작성하는 라이브러리. 컴파일 시점에 문법 오류를 잡고 동적 쿼리 작성에 강점이 있다.
(실무에서 널리 사용)
- JPA 벌크 업데이트 주의점 : JPQL 벌크 업데이트/삭제는 영속성 컨텍스트를 거치지 않고 DB에 직접 반영된다.
따라서 이미 1차 캐시에 올라온 엔티티 값과 DB 값이 불일치할 수 있다. 벌크 연산 후에는clear()로 영속성 컨텍스트를 비우거나 별도 트랜잭션으로 분리해 오염을 막는다.
용어정리 – Spring
트랜잭션과 AOP
-
@Transactional : Spring AOP를 통해 구현되어 있다.
@Transactional이 선언되면 해당 빈에 트랜잭션 프록시가 적용되고,
메서드 호출 전후로 트랜잭션 시작/커밋/롤백을 처리한다. 기본적으로RuntimeException과Error가 발생하면 롤백하고, Checked Exception은 롤백하지 않는다. Checked Exception까지 롤백하려면rollbackFor를 지정해야 한다. - @Transactional 특징
@Transactional은 우선순위를 가지고 있다. 메서드에 선언된 트랜잭션의 우선순위가 가장 높고, 인터페이스에 선언된 트랜잭션의 우선순위가 가장 낮다.- Spring의 기본 트랜잭션 AOP는 Proxy Mode이므로 프록시를 거친 메서드 호출에 트랜잭션이 적용된다.
readOnly = true는 쓰기 작업을 절대 막는 옵션이라기보다 읽기 전용 힌트에 가깝다.
JPA/Hibernate에서는 플러시 최적화 등에 영향을 줄 수 있으나 DB와 구현체에 따라 동작이 다르다.
-
적용되지 않는 대표 사례 : Spring의 기본 트랜잭션은 프록시 기반 AOP이므로 프록시를 거치지 않는 호출에는 동작하지 않는다.
같은 클래스 내부에서 메서드를 직접 호출하는 self-invocation,private메서드,final메서드가 대표적이다. 또한 기본 롤백 대상은RuntimeException과Error이며, Checked Exception까지 롤백하려면rollbackFor를 지정해야 한다. - @Transactional Propagation(트랜잭션 전파) : 이미 트랜잭션이 진행 중일 때 추가 트랜잭션 진행을 어떻게 할지 결정할 수 있는 옵션
- REQUIRED : 트랜잭션이 필요함(없으면 새로 만듦)
- SUPPORTS : 트랜잭션이 있으면 지원함(트랜잭션이 없어도 됨)
- MANDATORY : 트랜잭션이 의무임(트랜잭션이 반드시 필요함) / 없으면 IllegalTransactionStateException 예외 발생
- REQUIRES_NEW : 항상 새로운 트랜잭션이 필요함 / 기존 트랜잭션이 있다면 보류시키고 새로 만듦
- NOT_SUPPORTED : 트랜잭션 없이 진행함 / 기존 트랜잭션이 있다면 보류시킴
- NEVER : 트랜잭션을 사용하지 않음(기존 트랜잭션도 허용하지 않음)/ 트랜잭션이 있으면 IllegalTransactionStateException 예외 발생
- NESTED : 기존 트랜잭션 안에 Savepoint를 만들어 중첩 트랜잭션처럼 동작함. 내부 작업만 롤백하고 외부 트랜잭션은 계속 진행할 수 있다.
DB와 트랜잭션 매니저가 Savepoint를 지원해야 한다.
- 물리 트랜잭션과 논리 트랜잭션 : DB 커넥션에 실제로 시작되는 트랜잭션을 물리 트랜잭션이라고 한다.
스프링에서@Transactional메서드 단위로 관리되는 트랜잭션 범위를 논리 트랜잭션이라고 한다.REQUIRED전파에서는 여러 논리 트랜잭션이 하나의 물리 트랜잭션에 참여할 수 있으며, 내부 논리 트랜잭션이 롤백 전용으로 표시되면 최종 커밋 시 전체 물리 트랜잭션이 롤백될 수 있다.- 물리 트랜잭션 : 실제 데이터베이스에 적용되는 트랜잭션으로, 커넥션을 통해 커밋/롤백하는 단위
- 논리 트랜잭션 : 스프링이 트랜잭션 매니저를 통해 트랜잭션을 처리하는 단위
-
전파 옵션의 비용과 함정 :
REQUIRES_NEW는 외부 트랜잭션을 잠시 멈추고 별도 커넥션을 사용한다.
반복 호출하거나 커넥션 풀이 작은 환경에서 사용하면 기존 트랜잭션이 커넥션을 잡은 채 새 커넥션을 기다려 풀 고갈 또는 데드락 위험을 키울 수 있다.NESTED는 savepoint 기반이며 JPA·DB·트랜잭션 매니저 조합에 따라 지원 여부가 다르다. 전파 옵션은 독립 커밋이 반드시 필요한지, 실패가 외부 작업에 전파돼야 하는지를 먼저 정한 뒤 사용한다. -
AOP(Aspect Oriented Programming) : 로깅, 트랜잭션, 보안처럼 여러 곳에 반복되는 공통 관심사를 핵심 로직과 분리하는 방식이다.
Spring에서는 주로 프록시 기반으로 메서드 호출을 가로채 부가 기능을 적용한다. - AOP 특징
- Pointcut에 매칭되는 스프링 빈에는 프록시 객체를 생성하고 타겟 객체(Target) 대신 프록시 빈으로 등록
- 생성된 프록시는 타겟 객체(Target)의 호출을 가로채고 Advice의 기능을 호출 후에 타겟 객체(Target)의 기능을 호출한다.
- AOP 주요 요소
- Target : 부가기능을 부여할 대상 (핵심기능을 담고 있는 모듈)
- Aspect : 부가기능을 정의한 모듈 , Advice 와 PointCut을 갖고 있다.
- Advice : 실질적으로 부가기능을 담은 구현체
- PointCut : 부가기능이 적용될 대상(Method)을 선정하는 방법
- JoinPoint : Advice가 적용될 수 있는 위치
Spring 핵심 원리
-
Spring Framework의 핵심 : 자바 엔터프라이즈용 경량 프레임워크다. 핵심 기술은 IoC/DI, AOP, PSA이며 비즈니스 로직에 집중하게 돕는다.
- Spring의 레이어드 아키텍처 : 하나의 레이어는 자신의 고유 역할을 수행하고, 인접한 다른 레이어에 무언가를 요청하거나 응답한다. 각 레이어는 다른 레이어를 신경 쓸 필요가 없기 때문에 자신의 역할에 충실할 수 있다. (특정한 레이어의 기능을 개선하거나 교체할 수 있기 때문에 재사용성과 유지 보수에 유리)
- Persistence Layer : 데이터 관련 처리를 담당하는 부분, repository
- Application Layer : 비즈니스 핵심 로직을 처리하는 부분, service
- Presentation Layer : view를 담당하는 부분, controller
- Spring DI/IoC
- IoC(제어의 역전) : 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것으로 코드의 최종호출은 개발자가 제어하는 것이 아닌 프레임워크의 내부에서 결정된 대로 이루어진다.
- DI(의존관계 주입) : Spring 프레임워크에서 지원하는 IoC의 형태로, 클래스 사이의 의존관계를 빈 설정 정보를 바탕으로 컨테이너가 자동으로 연결하는 것
- 생성자 주입 : 필수 의존관계를 생성 시점에 전달하고 필드를
final로 선언할 수 있어 불변성과 테스트 편의성이 좋다. 누락된 의존성을 객체 생성 시점에 확인할 수 있고 생성자 기반 순환 참조도 애플리케이션 시작 단계에서 드러난다. - 수정자(Setter) 주입 : 선택, 변경 가능성이 있는 의존관계에 사용되며 스프링빈을 선택적으로 등록한다.
- 필드 주입 :
@Autowired를 사용하는데 외부에서 변경이 불가능하여 테스트 하기 힘들다. DI 프레임워크 없이는 작동하기 힘들며,
주로 애플리케이션과 관계없는 테스트코드나@Configuration같은 스프링 설정 목적으로 사용
- 생성자 주입 : 필수 의존관계를 생성 시점에 전달하고 필드를
-
POJO(Plain Old Java Object) : 특정 기술/프레임워크에 종속되지 않는 순수 자바 객체. 스프링은 POJO 기반 개발을 지향한다.
-
PSA(Portable Service Abstraction) : 일관된 추상화를 제공해 구현 기술을 바꿔도 코드 변경을 최소화하는 것. (예:
@Transactional은 JPA/JDBC 등 어떤 기술이든 동일하게 동작,PlatformTransactionManager추상화) - Spring vs Spring Boot : Spring Boot는 Spring을 더 쉽게 쓰기 위한 도구로, 복잡한 설정을 자동화한다.
- 자동 설정(Auto Configuration) :
@EnableAutoConfiguration은 classpath를 감지해 필요한 빈을 자동 등록한다.
@SpringBootApplication에 포함되어 있다.@Conditional기반으로 조건에 맞을 때만 설정이 적용된다. - 내장 WAS(Embedded Tomcat) : 별도 WAS 설치 없이 jar 하나로 실행 가능
- Starter 의존성 :
spring-boot-starter-web처럼 관련 의존성을 묶어 버전 충돌 없이 관리 - @SpringBootApplication :
@Configuration+@EnableAutoConfiguration+@ComponentScan을 합친 어노테이션
- 자동 설정(Auto Configuration) :
- 스프링 컨테이너(Spring Container) : 스프링 빈의 생성, 의존성 주입, 생명주기를 관리하는 핵심 런타임이다.
개발자는 객체 생성과 의존관계 연결을 직접 관리하지 않고, 컨테이너가 설정과 어노테이션을 기반으로 빈을 구성하도록 위임한다.- BeanFactory : 스프링 빈의 생성, 의존성 주입, 조회를 담당하는 핵심 컨테이너 인터페이스. 빈 생성 시점은 구현과 설정에 따라 달라진다.
- ApplicationContext :
BeanFactory기능에 이벤트 발행, 국제화, 리소스 조회 등 애플리케이션 기능을 더한 컨테이너. 일반적으로 초기화할 때 지연 설정이 아닌 싱글톤 빈을 미리 생성한다.
- Spring Bean : 스프링 컨테이너에 의해 생성되고 관리되는 자바 객체를 뜻하며, 스프링 컨테이너는 하나 이상의 빈(Bean)을 관리한다.
- Spring Bean 생명주기 : 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 메서드 호출 → 사용 → 제거 콜백 메서드 호출 → 스프링 종료
- Spring Bean 콜백(초기화 및 소멸) :
- @PostConstruct : 초기화 콜백 (의존관계 주입이 끝나면 호출)
- @PreDestroy : 제거 콜백 (메모리 반납, 연결 종료와 같은 과정)
- Spring Bean 콜백(초기화 및 소멸) :
- Spring Bean 생명주기 : 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 메서드 호출 → 사용 → 제거 콜백 메서드 호출 → 스프링 종료
- Spring Bean 등록 :
@ComponentScan은@Component클래스를 자동 등록한다.
@Configuration클래스와@Bean메서드로도 직접 등록할 수 있다.@Bean(메서드) 을 사용하여 빈 설정파일에 직접 빈을 등록할 수 있다.- Bean/Component 차이점
- @Bean : 개발자가 작성한 method를 기반으로 메서드에서 반환하는 인스턴스 객체 생성
- @Component : 개발자가 작성한 class를 기반으로 실행시점에 인스턴스 객체를 생성.
(
@Controller,@Service,@Repository는 모두@Component)
- Bean/Component 차이점
- Bean Scope : 빈 스코프는 빈이 존재할 수 있는 범위를 뜻하며 싱글톤(singleton), 프로토타입(prototype), request, session,
application 등이 있다.- singleton Scope : 스프링 빈 스코프의 기본값, 스프링 컨테이너의 시작과 종료까지 유지, 싱글톤 스코프의 빈을 조회하면,
스프링 컨테이너는 항상 같은 인스턴스를 반환한다. - prototype Scope : 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프, 항상 새로운 인스턴스를 생성해서 반환한다
- singleton Scope : 스프링 빈 스코프의 기본값, 스프링 컨테이너의 시작과 종료까지 유지, 싱글톤 스코프의 빈을 조회하면,
- 빈 충돌 해결 : 같은 타입의 빈이 여러 개일 때 주입 대상을 특정하는 방법
- @Primary : 여러 후보 중 우선적으로 주입될 빈을 지정
- @Qualifier : 주입 시 특정 이름의 빈을 명시적으로 선택
- 순환 참조(Circular Dependency) : A 빈이 B를, B 빈이 A를 서로 주입받는 상황이다.
- 생성자 주입은 빈 생성 시점에 순환 참조를 감지해 애플리케이션 구동을 실패시킨다.
- Spring Boot 2.6+는 기본적으로 순환 참조를 금지한다.
- 런타임 장애를 예방하려면 설계를 분리하는 것이 정석이다.
Spring MVC와 요청 처리
- Spring MVC 요청 처리 흐름 클라이언트 요청 → DispatcherServlet → HandlerMapping → Controller 탐색 → HandlerAdapter 실행 → Controller 결과 반환 → ViewResolver 결정 (또는
@ResponseBody/@RestController면 HttpMessageConverter가 JSON 직렬화) → 응답 반환- DispatcherServlet : 모든 요청을 받아 적절한 핸들러로 분배하는 프론트 컨트롤러
- @RestController :
@Controller+@ResponseBody. View가 아닌 데이터(JSON)를 반환
- Servlet Filter : 서블릿 실행 전, 후에 어떤 작업을 하고자 할때 사용한다.
이 필터가 있음으로써 WAS에서 설정을 변경하지 않고도 모든 서블릿에 영향을 준다.- Filter 에러 처리 : Filter 예외는
@ControllerAdvice가 직접 처리하지 않는다. Filter에서 직접 응답을 만들거나 error dispatch를 통해ErrorController로 전달하도록 구성한다.
- Filter 에러 처리 : Filter 예외는
- Spring Interceptor : DispatcherServlet 내부에서 컨트롤러 실행 전후에 공통 로직을 처리한다. 인증, 로깅,
권한 체크처럼 Spring MVC 핸들러 정보가 필요한 작업에 적합하다.- Interceptor 에러 처리 : DispatcherServlet의 예외 처리 흐름을 타는 예외는
@ControllerAdvice로 처리할 수 있다.
- Interceptor 에러 처리 : DispatcherServlet의 예외 처리 흐름을 타는 예외는
Spring Security와 인증
-
인증(Authentication)과 인가(Authorization) : 인증은 “누구인지”를 확인하는 것(로그인),
인가는 “무엇을 할 수 있는지” 권한을 확인하는 것(접근 제어). -
Spring Security 인증 과정 인증 요청 → AuthenticationFilter → UsernamePasswordAuthenticationToken 생성 → AuthenticationManager 인증 → UserDetailsService 조회 → SecurityContext 저장 이후 UserDetailsService 구현체를 통해 DB에 저장된 정보와 비교해 일치하면 UserDetails 구현 객체를 반환해 SecurityContext에 저장한다.
- Spring Security 구조
- SecurityFilterChain : 여러 보안 필터(Filter)들의 모음. 요청은 이 필터 체인을 거쳐 인증/인가가 처리된다.
(과거WebSecurityConfigurerAdapter는 deprecated → 현재는SecurityFilterChain빈으로 설정) - SecurityContextHolder : 인증된 사용자 정보(Authentication)를 보관하는 곳. 기본적으로 ThreadLocal에 저장된다.
- PasswordEncoder : 비밀번호를 단방향 해시로 암호화 (BCrypt 권장)
- SecurityFilterChain : 여러 보안 필터(Filter)들의 모음. 요청은 이 필터 체인을 거쳐 인증/인가가 처리된다.
- JWT(Json Web Token) : 인증/인가에 필요한 클레임을 담고 서명한 토큰이다.
서버가 세션 상태를 저장하지 않아도 요청마다 토큰 검증으로 사용자를 식별할 수 있어 Stateless 구조에 자주 사용된다.- Header(헤더) : 토큰 타입(
typ), 서명 알고리즘(alg), 선택적으로 서명 키를 식별하는 ID(kid) 등의 정보가 담긴다. - Payload(페이로드) : 토큰에서 사용할 정보의 조각들인 클레임(Claim)이 담겨 있다. 클레임(Claim)은 Key/Value 형태로 된 값을 갖는다.
- Signature(서명) : Base64URL로 인코딩한 Header와 Payload를 서명 키로 서명한 값이다. 토큰이 변조되지 않았고 신뢰하는 주체가 발급했는지 검증하는 용도이며, Payload를 암호화하지는 않는다.
- Header(헤더) : 토큰 타입(
- JWT의 동작 방식
- 인증(Authentication) : 로그인 성공 시 서버가 JWT를 발급하고, 클라이언트는 이후 요청에 토큰을 포함한다.
- 인가(Authorization) : 서버는 토큰의 서명과 클레임을 검증해 사용자와 권한을 판단한다.
- JWT의 장점
- 자가 포함(Self-contained) : 필요한 클레임을 토큰에 담을 수 있어 별도 세션 조회를 줄인다.
- 확장성 : 서버 상태 의존이 작아 여러 인스턴스나 서비스 간 인증 처리에 유리하다.
- 주의점 : Payload는 암호화가 아니라 인코딩이므로 민감 정보를 넣으면 안 된다.
-
토큰 수명과 폐기 전략 : Access Token은 탈취 피해 범위를 줄이기 위해 짧은 만료 시간을 두고,
Refresh Token은 서버 측 저장소에서 회전(rotation)·폐기·재사용 탐지를 관리한다. 권한 변경이나 강제 로그아웃을 즉시 반영해야 하면 JWT만으로는 부족하므로 토큰 버전, deny-list, 세션 저장소 등 상태 확인 수단이 필요하다. 검증 시에는 서명뿐 아니라 만료 시간(exp), 발급자(iss), 대상(aud), 허용 알고리즘을 검증하고, 클라이언트가 보낸 알고리즘을 그대로 신뢰하지 않는다. -
브라우저 보안 경계 : 쿠키 기반 인증은 CSRF 방어가 필요하며
SameSite, CSRF 토큰, Origin/Referer 검증을 조합한다.
토큰을 JavaScript가 읽을 수 있는 저장소에 두면 XSS 탈취 위험이 커지고,HttpOnly쿠키는 XSS 노출을 줄이는 대신 CSRF 대책이 필요하다. CORS는 서버 간 보안 장치가 아니라 브라우저의 교차 출처 읽기 제한이므로, 인증·인가를 대신하지 않는다. - 세션 기반 인증 vs 토큰(JWT) 기반 인증
- 세션 방식 : 서버가 세션을 저장(Stateful). 서버 확장(Scale-out) 시 세션 공유 문제가 있어 세션 클러스터링이나 Redis 세션 스토리지가 필요.
- JWT 방식 : 서버가 상태를 저장하지 않음(Stateless). 확장에 유리하나 토큰 탈취/만료 관리가 과제 → Access Token + Refresh Token 조합으로 보완.
- JWT 주의점 : JWT는 서명된 토큰이지 기본적으로 암호화된 토큰이 아니다.
Payload는 Base64URL로 인코딩되어 쉽게 읽을 수 있으므로 민감 정보를 넣으면 안 된다. 또한 서버가 토큰 상태를 저장하지 않기 때문에 강제 로그아웃, 토큰 폐기, 권한 변경 즉시 반영이 어렵다.
- OAuth 2.0 : 제3자 애플리케이션에 비밀번호를 노출하지 않고 자원 접근 권한을 위임하는 인가 프레임워크. (소셜 로그인의 기반) Resource Owner,
Client, Authorization Server, Resource Server로 구성되며, 보통 Authorization Code Grant 방식을 사용한다. 사용자 인증 후 인증 코드를 받아 Access Token으로 교환한다.
외부 연동과 비동기 실행 안정성
-
Timeout : 외부 API, DB, Redis, Kafka 등 원격 호출에는 반드시 연결 시간(connect timeout)과 응답 대기 시간(read timeout)을 설정해야 한다. timeout이 없으면 장애가 난 외부 시스템 때문에 애플리케이션 스레드가 고갈될 수 있다.
-
Retry : 일시적인 네트워크 오류에는 재시도가 도움이 되지만, 무조건 재시도하면 장애 시스템에 부하를 더 줄 수 있다. 재시도 횟수 제한, 지수 백오프,
jitter, 멱등성 보장이 필요하다. -
Rate Limiting : 특정 사용자나 클라이언트의 과도한 요청을 제한해 시스템을 보호하는 방법. API Gateway, Nginx,
Redis 카운터 등을 이용해 구현하며, 처리 비용이 크거나 남용 위험이 큰 엔드포인트에 특히 중요하다. -
@Async : Spring AOP를 통해 구현되어 있다.
@Async가 선언되면 스프링이 프록시 객체를 만들어 비동기 실행을 위임한다. 프록시 기반이기 때문에private 메서드,final 메서드, self-invocation(같은 클래스 내부 호출)에서는 동작하지 않는다. 반드시@EnableAsync가 필요하며, 운영 환경에서는 명시적인 Executor 설정이 권장된다. - @Async의 Thread Pool
- SimpleAsyncTaskExecutor : 작업마다 새로운 스레드를 생성하고 비동기 방식으로 동작한다. 스레드 풀 방식이 아니므로 스레드를 재사용하지 않으며, 요청이 많으면 스레드가 과도하게 늘어날 수 있다.
- ThreadPoolTaskExecutor : 스레드 풀 기반의 TaskExecutor. 운영 환경에서는
corePoolSize,maxPoolSize,queueCapacity,RejectedExecutionHandler를 명시적으로 설정하는 것이 좋다.- ThreadPoolTaskExecutor 옵션
- corePoolSize : 기본적으로 유지할 스레드 개수
- maxPoolSize : 스레드 풀에 살아있는 최대 개수
- queueCapacity : 모든 core 스레드가 작업 중일 때 대기열에 쌓을 수 있는 작업 개수
- keepAliveSeconds : 스레드 풀 내 스레드 개수가 corePoolSize 초과인 상태에서, 대기 상태의 스레드가 종료되는 시간
- ThreadPool 작동방식
- 스레드 수가 corePoolSize보다 작으면 새 스레드를 만들어 작업을 실행한다.
- core 스레드가 모두 작업 중이면 queueCapacity만큼 큐에 작업을 쌓는다.
- 큐가 가득 차고 스레드 수가 maxPoolSize보다 작으면 추가 스레드를 만든다.
- 큐도 가득 차고 maxPoolSize에도 도달하면 거부 정책이 동작한다.
- ThreadPoolTaskExecutor 옵션
- @Async의 return :
void,Future,CompletableFuture타입을 리턴 타입으로 사용할 수 있다.- Future :
future.get()은 블로킹을 통해 요청 결과가 올 때까지 기다리는 역할을 한다. 그래서 비동기 블로킹 방식이 되어버려 성능이 좋지 않다. - CompletableFuture : Java 8에서 추가된 방식. 여러 연산을 결합할 수 있고, 연산이 완료되면 다음 단계의 작업을 수행하거나 예외를 처리할 수 있다. 최근 코드에서는
ListenableFuture보다CompletableFuture를 주로 사용한다.
- Future :
용어정리 – Java
Java 동시성
- 동시성 키워드: synchronized / volatile / atomic : 멀티스레드 환경에서 가시성(Visibility)과 원자성(Atomicity)을 다룬다.
- synchronized : 임계 영역에 한 번에 하나의 스레드만 진입하도록 락(lock)을 거는 키워드.
원자성과 가시성을 모두 보장하지만 락 경합으로 성능 비용이 있다. - volatile : 한 스레드의 쓰기가 이후 다른 스레드의 읽기에 보이도록 Java 메모리 모델의 가시성과 순서 규칙(happens-before)을 제공한다.
그러나
count++처럼 읽기-수정-쓰기 복합 연산의 원자성은 보장하지 못한다. - atomic (java.util.concurrent.atomic) :
AtomicInteger등은 CAS(Compare-And-Swap)를 활용해 개별 읽기·갱신 연산의 원자성과 가시성을 제공한다. 락 없이 구현된 연산이 많아 단순 카운터 등에 적합하지만, 여러 호출을 묶은 로직까지 자동으로 원자적이 되지는 않는다.
- synchronized : 임계 영역에 한 번에 하나의 스레드만 진입하도록 락(lock)을 거는 키워드.
-
동시성 제어 선택 기준 : 단순 상태 변경 전파처럼 가시성만 필요하면
volatile을 사용할 수 있지만,count++같은 복합 연산에는 적합하지 않다.
임계 영역 전체의 원자성과 가시성이 필요하면synchronized가 명확하고, 단순 카운터나 누적값처럼 짧은 원자 연산은AtomicInteger같은 atomic 계열이 락 경합을 줄이는 데 유리하다. -
CAS와 ABA 문제 : CAS는 값이 예상값과 같을 때만 변경하는 낙관적 방식이다.
값이A → B → A로 바뀌면 CAS는 값이 처음부터A였다고 오인할 수 있는데, 이를 ABA 문제라고 한다. 단순 카운터에는 보통 문제가 없지만 상태 전이가 중요한 자료구조에서는 버전 번호를 함께 비교하는AtomicStampedReference같은 방법을 검토한다. 또한 CAS는 경합이 심하면 반복 재시도로 CPU를 소모할 수 있어, 항상 락보다 빠른 것은 아니다. - 가시성과 안전한 공개 : 불변 객체는 생성자에서 완전히 초기화한 뒤
final필드로 보관하면 다른 스레드에 안전하게 공개하기 쉽다.
반면 일반 컬렉션을 여러 스레드가 동시에 수정하면volatile참조만으로는 안전하지 않으며, 동기화·동시성 컬렉션·메시지 전달 중 하나로 변경 범위를 보호해야 한다.ConcurrentHashMap도 개별 연산은 안전하지만 여러 연산을 묶은 업무 규칙까지 원자적으로 만들어 주지는 않는다.
Java 예외 처리
- Error와 Exception
- Error : JVM이나 실행 환경의 심각한 문제를 나타내는
Throwable계열이다. 애플리케이션에서 일반적으로 복구를 시도하지 않으며, 발생했다고 항상 즉시 프로세스가 종료되는 것은 아니다. - Exception : 애플리케이션이 처리하거나 호출자에게 전달할 수 있는 예외 상황을 나타내는
Throwable계열이다.- Checked Exception과 Unchecked Exception
- Checked Exception : RuntimeException을 상속하지 않고 반드시 에러 처리(try/catch or throw)를 해야한다.
(대표적으로 FileNotFoundException) - UncheckedException : RuntimeException을 상속하면 UncheckedException.
체크 예외와는 달리 에러 처리를 강제하지 않음 (대표적으로 NullPointerException)
- Checked Exception : RuntimeException을 상속하지 않고 반드시 에러 처리(try/catch or throw)를 해야한다.
- Checked Exception과 Unchecked Exception
- Error : JVM이나 실행 환경의 심각한 문제를 나타내는
JVM과 메모리
- JVM : 자바 바이트코드를 실행하는 가상 머신. 클래스 로딩, 바이트코드 실행, 메모리 관리(GC)를 담당한다.
- Class Loader : VM내로 클래스를 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈
- Execution engine(실행 엔진) : 바이트 코드를 실행시키는 역할
- Interpreter : 바이트 코드를 한 줄씩 실행한다.
- JIT 컴파일러 : 인터프리터가 반복되는 코드를 발견하면 JIT 컴파일러가 반복되는 코드를 네이티브 코드로 바꿔준다.
그 다음부터 인터프리터는 네이티브 코드로 컴파일된 코드를 바로 사용한다. - GC(Garbage Collector) : 힙 영역에서 사용되지 않는 객체들을 제거하는 작업을 수행
- Runtime Data Areas: 프로그램 실행 중에 사용되는 다양한 영역
- JNI(Java Native Interface): 자바 애플리케이션에서 C, C++, 어셈블리어로 작성된 함수를 사용할 수 있는 방법을 제공,
Native 키워드를 사용하여 메서드를 호출 (대표적인 메서드는 Thread의 currentThread()) - Native Method Library: C, C++로 작성된 라이브러리
- GC(Garbage Collector) : Heap에서 더 이상 도달할 수 없는 객체의 메모리를 회수하는 JVM 기능이다. 수집 범위와 알고리즘, Stop-the-world 구간은 사용하는 GC에 따라 다르다.
- Young GC(Minor GC) : 전통적인 세대별 GC에서는 Young 영역을 주로 Eden과 두 Survivor 공간으로 구성한다. 살아남은 객체를 Survivor 공간으로 복사하고, 일정 조건을 만족한 객체는 Old 영역으로 승격한다. 세부 구조와 동작은 GC 구현에 따라 다르다.
- Major GC와 Full GC : 용어는 JVM 구현과 도구마다 다르게 쓰인다. 일반적으로 Major GC는 Old 영역 수집을, Full GC는 Heap 전체 수집을 뜻하므로 둘을 같은 의미로 단정하면 안 된다. Mark, Sweep, Compact 사용 여부도 GC 알고리즘에 따라 다르다.
- Mark & Sweep & Compact & Promotion
- Mark: 접근 가능한 객체에 Mark하여 표시
- Sweep: Mark되지 않은 객체들을 제거하는 과정
- Compact: Sweep 과정에 의해 삭제되면 메모리 단편화가 발생하는데, Compact를 통해 빈자리들을 채워줌
- Promotion : Survivor 영역에서 계속해서 살아남은 객체들이 특정 age 값에 도달하면, Old Generation으로 이동하게 되는 과정
- GC(Garbage Collector) 알고리즘
- Serial GC : 서버의 CPU 코어가 1개일 때 사용하기 위해 개발된 GC, Stop The World 시간이 길다,
Mark & Sweep & Compact 알고리즘을 사용 - Parallel GC : Serial GC와 기본적인 알고리즘은 같지만, Young 영역의 Minor GC를 멀티 쓰레드로 수행
- G1 GC (Garbage First) : 기존의 GC 알고리즘에서는 Heap 영역을 물리적으로 고정된 Young/Old 영역으로 나누어 사용하였지만,
G1 GC는 Region이라는 개념을 도입하여 Heap을 여러 영역으로 나누어 관리한다. 전체 Heap을 한 번에 처리하지 않고 회수 효율이 높은 Region을 우선 수집하여 Stop The World 시간을 예측 가능하게 줄이는 것을 목표로 한다. (Java 9+ 기본 GC) - ZGC / Shenandoah : 대용량 Heap용 저지연(Low-Latency) GC다. Stop-The-World를 수 밀리초 이내로 줄이는 것을 목표로 한다.
대부분의 GC 작업(마킹, 재배치)을 애플리케이션 스레드와 동시에(Concurrent) 수행한다. ZGC는 수 TB 단위의 Heap까지 확장 가능한 것이 특징이다.
- Serial GC : 서버의 CPU 코어가 1개일 때 사용하기 위해 개발된 GC, Stop The World 시간이 길다,
-
GC 문제가 의심될 때 확인 순서 : 먼저 지표에서 GC pause time, GC frequency, heap 사용률, old 영역 증가 추세를 확인한다.
이후 GC 로그로 Minor/Full GC 빈도와 원인을 보고, 메모리 누수가 의심되면 Heap Dump의 dominator tree와 객체 참조를 분석한다. CPU가 높거나 요청이 멈추는 상황이면 Thread Dump로 데드락, 블로킹 I/O, 락 경합을 확인한다. - 자바의 메모리 영역
- Stack 영역 : 기본 자료형(원시 자료형, Primitive type), 지역변수, 매개변수가 저장되는 메모리 영역.
Heap 영역에 생성된 데이터의 참조값이 할당되며, 메서드가 호출될 때 메모리에 할당되고 메서드 종료 시 삭제된다. - Heap 영역 : 인스턴스를 생성(new)할 때 사용되는 메모리 영역,
참조형 데이터 객체의 실제 데이터가 저장되는 공간 (Stack 영역에서 실제데이터가 존재하는 Heap 영역의 참조값을 가지고 있다.), GC가 관리한다. - static 필드 : 클래스에 속해 모든 인스턴스가 공유하는 필드다. static 필드가 참조하는 객체는 Heap에 존재하며, 클래스 로더가 살아 있는 동안에는 보통 도달 가능하므로 GC 대상이 되지 않는다.
- Metaspace : 클래스 메타데이터(클래스 구조, 메서드 정보 등)가 저장되는 영역.
Java 8부터 기존의 PermGen(Permanent Generation)을 대체하며, Heap이 아닌 네이티브 메모리(OS 메모리)에 할당된다. 따라서 고정 크기였던 PermGen과 달리 필요에 따라 동적으로 크기가 늘어나OutOfMemoryError: PermGen space같은 문제를 줄였다.
- Stack 영역 : 기본 자료형(원시 자료형, Primitive type), 지역변수, 매개변수가 저장되는 메모리 영역.
Java 객체와 컬렉션
- 동일성(identity)와 동등성(equality)
- 동일성(identity) : 두 참조가 같은 객체를 가리키는지 비교하며
==연산자를 사용한다. JVM의 실제 메모리 주소를 직접 비교한다는 뜻은 아니다. - 동등성(equality) : 객체의 내부 값이 같음을 비교,
equals()메서드를 오버라이드하여 구현
- 동일성(identity) : 두 참조가 같은 객체를 가리키는지 비교하며
- equals()와 hashCode() 규약 : 두 메서드는 반드시 함께 오버라이드해야 한다.
- 규약 :
equals()로 같다고 판단되는 두 객체는 반드시 같은hashCode()를 반환해야 한다.
(역은 성립하지 않아도 됨 — hashCode가 같아도 equals는 다를 수 있다) - 왜 함께 오버라이드해야 하나(HashMap과의 연관) :
HashMap,HashSet등 해시 기반 컬렉션은 먼저hashCode()로 버킷을 찾고,
같은 버킷 안에서equals()로 최종 일치를 판단한다.equals()만 재정의하고hashCode()를 재정의하지 않으면, 논리적으로 같은 객체라도 서로 다른 버킷에 저장되어 조회/중복 제거가 정상 동작하지 않는다.
- 규약 :
- String / StringBuilder / StringBuffer
- String : 불변(Immutable) 객체. 문자열을 더할 때마다 새로운 객체가 생성되므로, 반복적인 문자열 연결에는 비효율적이다.
(String Constant Pool로 리터럴 재사용) - StringBuilder : 가변(Mutable) 객체. 문자열을 변경해도 같은 객체를 수정한다.
동기화하지 않아(non-synchronized) 단일 스레드 환경에서 빠르다. - StringBuffer : StringBuilder와 동일하나 메서드가 동기화(synchronized)되어 스레드 안전하다.
대신 동기화 비용으로 단일 스레드에서는 StringBuilder보다 느리다.
- String : 불변(Immutable) 객체. 문자열을 더할 때마다 새로운 객체가 생성되므로, 반복적인 문자열 연결에는 비효율적이다.
- 컬렉션 프레임워크(Collection Framework)
- List / Set / Map : List는 순서가 있고 중복을 허용, Set은 순서가 없고(일부 구현 제외) 중복을 허용하지 않음,
Map은 Key-Value 쌍으로 Key의 중복을 허용하지 않는다. - ArrayList vs LinkedList : ArrayList는 배열 기반이라
get이 O(1)로 빠르다. 중간 삽입·삭제에는 요소 이동이 필요하다.
LinkedList는 노드 연결 기반이라 양 끝 또는 이미 찾은 노드의 삽입·삭제는 빠르지만, 특정 위치를 먼저 찾는 데 O(n)이 걸린다. - HashMap 내부 동작 : Key의
hashCode()로 버킷(bucket) 위치를 정하고 그 안에 저장한다.
서로 다른 Key가 같은 버킷에 들어가는 해시 충돌(Collision) 시에는 같은 버킷의 엔트리를 연결해 관리한다. Java 8부터는 엔트리가 임계값(기본 8개)에 도달하고 테이블 용량도 최소 기준(기본 64)을 만족하면 레드-블랙 트리로 전환(Treeify) 하여 충돌이 많은 버킷의 조회 성능을 개선한다.
- List / Set / Map : List는 순서가 있고 중복을 허용, Set은 순서가 없고(일부 구현 제외) 중복을 허용하지 않음,
Java 기본 문법과 실행 환경
- Stream API : 컬렉션 등의 데이터를 내부 반복으로 정렬·필터링·변환하는 API. Stream 연산 자체는 원본 컬렉션을 변경하지 않지만, 람다 내부의 부작용으로 요소 객체나 외부 상태를 변경하지 않도록 주의해야 한다.
parallel()로 병렬 처리도 가능하다.- Mutable 객체 : 생성된 이후에도 객체 내부 상태를 변경할 수 있는 객체다. setter는 상태 변경 방법 중 하나일 뿐 필수 조건은 아니다.
- Immutable 객체 : 생성된 이후 수정 불가능하며, 변경이 필요하면 새 객체를 만든다. 내부에 가변 객체를 노출하지 않고 안전하게 공개하면 스레드 안전성에 유리하다. 자바의 대표적인 불변 객체는
String이다.
- 함수형 인터페이스와 람다
- 함수형 인터페이스(Functional Interface) : 추상 메서드가 하나인 인터페이스다.
@FunctionalInterface로 명시할 수 있다.
(대표적으로Runnable,Comparator,Function,Supplier,Consumer,Predicate) - 람다 표현식(Lambda) : 함수형 인터페이스의 구현을 익명 함수 형태(
(x) -> x + 1)로 간결하게 표현하는 문법. - 메서드 레퍼런스(Method Reference) : 람다가 단순히 기존 메서드를 호출하기만 할 때 더 간결하게 표현하는 문법.
(System.out::println,String::valueOf,User::getName)
- 함수형 인터페이스(Functional Interface) : 추상 메서드가 하나인 인터페이스다.
- 제네릭(Generics) : 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법,
객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄여주며 재사용성을 높인다. 제네릭은 참조 타입에서만 사용할 수 있으며 원시 타입에서 사용하고 싶다면 래퍼(Wrapper) 클래스를 사용하여야 한다.- 와일드카드 : 와일드카드는 ? 문자를 사용하여 불특정 타입을 나타낼 때 사용되며 주로 메서드 파라미터에 적용한다.
- 무제한 와일드카드 : 어떤 타입이라도 허용 (List<?> list)
- 상한 경계 와일드카드 (Upper Bounded Wildcard) : 특정 타입의 하위 클래스만 허용 (List<? extends Number> list)
- 하한 경계 와일드카드 (Lower Bounded Wildcard) : 특정 타입의 상위 클래스만 허용 (List<? super Integer> list)
- 와일드카드 : 와일드카드는 ? 문자를 사용하여 불특정 타입을 나타낼 때 사용되며 주로 메서드 파라미터에 적용한다.
- 리플렉션(Reflection) : 런타임 시에 자바 클래스의 메타데이터(metadata)를 검사하고 조작할 수 있게 해주는 기능, 이를 통해 클래스, 인터페이스, 필드,
메서드 등의 정보를 얻고, 해당 객체의 메서드를 호출하거나 필드 값을 수정할 수 있다.- 클래스 로딩 및 인스턴스화 : 런타임 시에 클래스 이름을 통해 클래스를 로딩하고, 인스턴스를 생성
- 메타데이터 접근 : 클래스의 필드, 메서드, 생성자 등의 메타데이터에 접근
- 필드 및 메서드 조작: : 객체의 필드 값을 읽거나 수정 또는 메서드를 호출
- 런타임 동적 바인딩 : 런타임에 동적으로 메서드나 필드를 바인딩하여 호출. (주로 플러그인 시스템, 의존성 주입, 프레임워크 개발 등에 사용)
- 직렬화(Serialization)과 역직렬화(Deserialization)
- 직렬화(Serialization) : 객체들의 데이터를 연속적인 데이터(스트림)로 변형하여 전송 가능한 형태로 만드는 것 (객체 데이터를 JSON으로 바꾼다.)
- 역직렬화(Deserialization) : 직렬화된 데이터를 다시 객체의 형태로 만드는 것 (JSON 데이터를 객체로 바꾼다)
- record / Optional 사용 의도
- record (Java 16+) : 불변 데이터 전달용 객체(DTO/VO)를 간결하게 정의하기 위한 타입. 필드 선언만으로 생성자, getter,
equals()/hashCode()/toString()이 자동 생성된다. 불변 데이터를 보일러플레이트 없이 표현하려는 의도. - Optional : 값이 없을 수 있음(null 가능성)을 타입으로 명시해 NullPointerException을 예방하려는 의도.
주로 메서드 반환 타입에 사용하며, 필드/파라미터로 남용하는 것은 권장되지 않는다.
- record (Java 16+) : 불변 데이터 전달용 객체(DTO/VO)를 간결하게 정의하기 위한 타입. 필드 선언만으로 생성자, getter,
- Final : final 키워드는 변수(variable), 메서드(method), 또는 클래스(class)에 사용될 수 있으며 프로그램 실행 도중에 수정할 수 없다.
- final 변수 : 한 번 초기화되면 그 이후에 변경할 수 없다.
- final 메서드 : 오버라이딩을 금지한다.
- final 클래스 : 다른 클래스에서 상속할 수 없다.
-
static final : 클래스에 하나만 존재하며 한 번만 초기화할 수 있는 필드다. 선언 시 또는 static initializer에서 초기화할 수 있다.
-
자바 원시타입 :
boolean,char(2byte),byte(1byte),short(2byte),int(4byte),
long(8byte),float(4byte),double(8byte).boolean은 JVM 명세상 정확한 크기가 고정되어 있지 않고 구현에 따라 달라질 수 있다. -
JDK와 JRE : JDK는 Java Development Kit의 약자로 개발에 필요한 도구(
javac,javadoc, 디버깅 도구 등)와 런타임을 포함한다.
JRE는 Java Runtime Environment의 약자로 자바 프로그램 실행에 필요한 런타임 환경이다. Java 11 이후에는 별도 JRE 배포보다 JDK 배포와jlink를 통한 커스텀 런타임 구성이 일반적이다. - Java의 실행방식
- 자바 컴파일러(javac)가 자바 소스코드(.java)를 읽어 자바 바이트코드(.class)로 변환
- Class Loader를 통해 class 파일들을 JVM으로 로딩.
- 로딩된 class파일들은 Execution engine을 통해 해석
- 해석된 바이트코드는 Runtime Data Areas 에 배치되어 수행
- Java 버전별 특징
- Java 8 : Lambda, Stream API, Optional, 새로운 날짜/시간 API(
java.time) 추가 - Java 11 (LTS) : HTTP Client 표준화,
var를 람다 파라미터에 사용 가능,javac로 컴파일하지 않고 단일 소스 파일 실행 가능.
Java 9부터 G1 GC가 기본 GC가 되었고, Java 11은 장기 지원 버전으로 많이 사용된다. - Java 17 (LTS) : Sealed Class, Pattern Matching for
instanceof, Text Blocks 등 추가.
Spring Boot 3의 최소 요구 버전이 Java 17이다. - Java 21 (LTS) : Virtual Threads (Project Loom), Sequenced Collections,
Pattern Matching forswitch, Record Patterns 추가 - Virtual Threads : 운영체제 스레드와 1:1 매칭되지 않는 경량 스레드로,
수백만 개의 스레드를 생성하여 I/O 블로킹 상황에서 효율적인 리소스 사용 가능
- Java 8 : Lambda, Stream API, Optional, 새로운 날짜/시간 API(
용어정리 – 성능 및 운영
커넥션 풀과 스레드 풀
-
Connection Pool : DB 연결을 매 요청마다 새로 만들지 않고 미리 만들어 둔 연결을 재사용하는 방식. 풀 크기가 너무 작으면 대기 시간이 늘고,
너무 크면 DB가 감당하지 못한다. 애플리케이션 인스턴스 수까지 고려해 전체 DB 커넥션 수를 계산해야 한다. -
HikariCP 주요 설정 :
maximumPoolSize,minimumIdle,connectionTimeout,idleTimeout,
maxLifetime이 중요하다. DB의 wait timeout보다maxLifetime을 짧게 잡아 죽은 커넥션을 재사용하지 않도록 한다. -
Thread Pool : 요청 처리나 비동기 작업을 제한된 스레드로 처리하는 방식. 스레드가 부족하면 큐 대기가 늘고,
큐가 무제한이면 장애 시 메모리 사용량이 급증할 수 있다. 작업 특성(CPU bound / I/O bound)에 따라 풀 크기와 큐 크기를 다르게 잡아야 한다. -
Pool 고갈 대응 : 대기 시간 증가, timeout, active connection/thread 수, queue size를 함께 본다. 원인은 느린 쿼리,
외부 API 지연, 락 경합, 트래픽 급증일 수 있으므로 지표와 trace로 병목 구간을 먼저 좁힌다.
Redis와 캐시 전략
-
Redis (Remote Dictionary Server) : 인메모리 기반 Key-Value 저장소. 캐시, 세션 저장소, 분산 락, 랭킹 등에 사용된다.
명령 처리는 주로 단일 이벤트 루프에서 실행되므로 무거운 명령(KEYS *, 큰 범위SORT)은 피해야 한다. - Redis 캐싱 전략 (읽기)
- Cache Aside (Look Aside) : 앱이 캐시를 먼저 확인하고(Cache Hit 시 반환), 없으면(Cache Miss) DB에서 조회 후 캐시에 저장.
가장 일반적인 방식. 캐시 장애가 DB 조회로 이어져 시스템이 죽지는 않지만, 최초 조회 시 지연이 발생한다. - Read Through : 애플리케이션은 캐시만 조회하고, 캐시가 내부적으로 DB를 조회해 값을 채우는 방식. 캐시 계층에 데이터 로딩 책임이 들어간다.
- Cache Aside (Look Aside) : 앱이 캐시를 먼저 확인하고(Cache Hit 시 반환), 없으면(Cache Miss) DB에서 조회 후 캐시에 저장.
- Redis 캐싱 전략 (쓰기)
- Write Through : 캐시와 DB에 동시에(캐시 갱신 후 DB 저장) 데이터를 저장.
데이터 정합성은 좋으나 매 쓰기마다 두 곳에 저장하므로 쓰기 속도가 느리다. - Write Back (Write Behind) : 데이터를 캐시에 먼저 저장하고 일정 시간/일정량이 모이면 DB에 배치(Batch) 저장.
쓰기 성능은 매우 좋으나 DB 반영 전 캐시 장애 시 유실 위험이 있다. - Write Around : 쓰기는 DB에만 하고 캐시는 갱신하지 않음. 이후 조회 시점에 캐시에 적재. 자주 읽히지 않는 데이터에 적합.
- Write Through : 캐시와 DB에 동시에(캐시 갱신 후 DB 저장) 데이터를 저장.
- 캐시 장애 상황
- Cache Stampede (Thundering Herd) : 인기 키가 동시에 만료되면,
수많은 요청이 한꺼번에 DB로 몰려(Cache Miss) DB에 부하가 폭증하는 현상. 해결책으로 만료 시간 분산(TTL Jitter), 갱신 시 분산 락(PER 알고리즘, 단일 스레드만 재계산), 논블로킹 재계산 등이 있다. - Cache Penetration(캐시 관통) : 캐시와 DB 모두에 없는 데이터를 반복 조회해 매번 DB까지 도달하는 현상.
→ 빈 결과도 캐싱하거나 Bloom Filter로 존재 여부 사전 차단. - Cache Avalanche(캐시 눈사태) : 다수의 키가 동시에 만료되거나 캐시 서버 자체가 다운되어 부하가 일시에 DB로 몰리는 현상. → TTL 분산,
다중화(Replica), 서킷 브레이커.
- Cache Stampede (Thundering Herd) : 인기 키가 동시에 만료되면,
- 캐시 일관성과 무효화(Invalidation) : “캐시 무효화는 컴퓨터 과학의 어려운 문제 중 하나”로 불릴 만큼 까다롭다.
DB 갱신 시 캐시를 어떻게 동기화할지가 핵심.- TTL(Time To Live) : 만료 시간을 두어 일정 시간 뒤 자동으로 캐시를 비우는 가장 단순한 방법
- Cache Eviction 정책 : 메모리가 가득 찼을 때 데이터를 제거하는 정책. LRU(가장 오래 안 쓰인 것), LFU(가장 적게 쓰인 것),
TTL 기반 등 - Write 시 캐시 갱신/삭제 : 데이터 변경 시 캐시를 갱신(Update)하거나 삭제(Delete)하여 정합성 유지.
일반적으로 갱신보다 삭제(Cache Invalidation) 후 다음 조회 시 재적재가 안전하다.
-
캐시 도입 시 판단 기준 : 캐시는 성능을 높이지만 정합성과 장애 전파 문제를 만든다. TTL, 무효화 정책, 캐시 미스 시 DB 보호 전략을 함께 설계해야 한다.
인기 키 동시 만료는 Cache Stampede로 이어질 수 있으므로 TTL jitter, 분산 락, 비동기 재계산을 고려하고, 캐시 장애 시 DB가 버틸 수 있도록 circuit breaker, rate limit, fallback을 준비한다. - Redis 자료구조 : String, List, Set, Sorted Set(ZSet), Hash, Bitmap, HyperLogLog, Geo,
Stream 등을 지원한다.- Sorted Set : score 기반 정렬 → 실시간 랭킹/리더보드에 활용
- Hash : 객체(필드-값) 저장에 적합
- Set : 중복 제거, 교집합/합집합 연산 (공통 관심사, 태그)
-
Redis 분산 락 (Distributed Lock) : 여러 서버/인스턴스가 공유 자원에 동시에 접근하는 것을 막기 위해 사용.
단일 Redis에서는 보통SET key value NX PX ttl형태로 락 획득과 만료 시간 설정을 원자적으로 처리한다.value에는 예측 불가능한 고유 토큰을 넣고, 해제 시에는 토큰이 일치할 때만 삭제하는 Lua Script를 사용한다. TTL이 지난 뒤 다른 요청이 같은 키를 획득할 수 있으므로 단순DEL은 이전 보유자가 새 보유자의 락을 지우는 오류를 만든다. - Redis 락의 보장 범위와 장애 모델
SET NX PX의 원자성은 해당 Redis 인스턴스 안에서만 보장된다.- 비동기 복제 전 Primary가 장애 나면, 승격된 Replica는 기존 락을 모를 수 있다.
- 따라서 단일 Redis 락은 장애 상황까지 포함한 강한 상호 배제를 보장하지 않는다.
- 캐시 재생성과 중복 실행이 치명적인 작업을 구분해야 한다.
- 락 만료와 소유권
- 작업 시간이 TTL보다 길면, 만료 후 다른 요청이 진입할 수 있다.
- TTL은 작업 시간의 상한과 네트워크 지연을 고려해 정한다.
- 연장 시에는 소유 토큰을 검증하고, 실패하면 락을 잃었을 수 있다고 판단한다.
- 중요한 쓰기는 DB 트랜잭션 또는 조건부 갱신으로 최종 무결성을 보장한다.
- Redlock의 선택 기준
- 독립 Redis 노드의 과반수(quorum)에서 제한 시간 안에 락을 얻어야 성공이다.
- 획득에 실패하거나 시간이 TTL을 넘기면 이미 얻은 락을 해제한다.
- 노드별 짧은 timeout, 재시도 제한, 지수 백오프와 jitter를 적용한다.
- 네트워크 분할, 긴 GC pause, 지연된 패킷의 위험은 남는다.
- 강한 정합성이 필요하면 DB 락, ZooKeeper, etcd 또는 fencing token을 검토한다.
- Fencing Token(펜싱 토큰)
- 락을 얻을 때 단조 증가하는 번호를 함께 받는다.
- 보호 대상 저장소는 이전 번호의 쓰기를 거부한다.
- TTL 만료 후 늦게 도착한 이전 보유자의 요청도 차단할 수 있다.
- 대상 저장소가 토큰 비교를 지원해야 하며, Redis 락만으로 자동 제공되지는 않는다.
- Redis 영속성(Persistence) : 인메모리지만 재시작 시 데이터 복구를 위해 디스크 저장 옵션을 제공한다.
- RDB (Snapshot) : 특정 시점의 메모리 전체를 스냅샷으로 저장. 파일이 작고 복구가 빠르지만, 스냅샷 사이의 데이터는 유실될 수 있다.
- AOF (Append Only File) : 모든 쓰기 명령을 로그로 기록. 유실이 적고 안정적이지만 파일이 크고 복구가 상대적으로 느리다.
(실무에서는 RDB + AOF 혼용)
- Redis 고가용성 / 확장
- Replication(복제) : Master-Replica 구조로 데이터를 복제해 읽기 분산과 장애 대비
- Sentinel(센티넬) : Master 장애를 감지해 Replica를 자동으로 승격(Failover)시켜 고가용성을 확보.
비동기 복제 기반이므로 장애 시 일부 데이터 유실 가능성이 있다. - Cluster(클러스터) : 데이터를 16384개의 해시 슬롯으로 샤딩(Sharding)하여 여러 노드에 분산 저장 → 수평 확장
장애 분석과 관측 가능성
- JVM 튜닝 및 장애 대응
- Heap Dump : 특정 시점의 JVM 메모리 상태를 기록한 파일. 메모리 누수(Memory Leak) 분석 시 사용.
- Thread Dump : 특정 시점의 모든 스레드 상태를 기록한 파일. 데드락(Deadlock)이나 CPU 점유율 이상 분석 시 사용.
- Observability (관측 가능성)
- Logging : 발생한 이벤트 기록 (ELK 스택 등)
- Metrics : 수치화된 데이터 기록 (Prometheus, Grafana)
- Tracing : 분산 환경에서 요청의 흐름 추적 (Micrometer Tracing, Jaeger, Zipkin)
-
성능 수치 해석 원칙 : 평균 응답 시간은 일부 매우 느린 요청을 가릴 수 있으므로 p50·p95·p99를 함께 본다. 처리량(TPS)이 높아져도 오류율, 타임아웃,
큐 대기 시간, DB 커넥션 대기가 증가했다면 실제 개선이 아닐 수 있다. 변경 전후 비교는 데이터 규모, 동시 사용자 수, 워밍업 여부, 캐시 상태, 요청 분포를 같게 맞추고 측정 기간과 환경을 함께 남긴다. -
장애 분석의 최소 증거 : 장애 시각을 기준으로 애플리케이션 로그의 요청 ID/trace ID, 오류율·지연 그래프, JVM/커넥션 풀/스레드 풀 지표,
DB의 느린 쿼리와 락 대기, 외부 의존성의 timeout을 함께 대조한다. 증상만으로 원인을 단정하지 않고, 병목 구간을 재현하거나 관련 지표로 반증해 좁힌다. -
운영 이슈 Trouble Shooting 절차 : 고객센터나 운영팀에서 이슈가 들어오면 먼저 영향 범위와 심각도를 분류한다.
전체 장애인지 특정 고객·테넌트·브라우저·기능 문제인지 나누고, 발생 시각, 요청 ID, 사용자 ID, 입력값, 최근 배포 여부를 모은다. 이후 로그, APM trace, DB slow query, 외부 연동 지표를 시간축으로 맞춰 원인을 좁힌다. 임시 우회와 근본 수정은 분리하고, 재발 방지 항목을 Jira 같은 이슈 관리 도구에 남긴다. -
Bugfix와 개선 작업 우선순위 : 운영/개발 업무에서는 모든 요청을 선입선출로 처리하면 중요한 장애가 묻힐 수 있다.
고객 영향도, 매출·업무 중단 여부, 보안/개인정보 위험, 발생 빈도, 우회 가능 여부를 기준으로 우선순위를 정한다. 장애성 이슈는 재발 방지까지 완료 기준에 포함하고, 성능 개선은 p95/p99 지연, slow query 감소, CS 감소 같은 측정 지표로 효과를 확인한다. - 운영형 솔루션 개발(SM)에서 중요한 역량 : 신규 기능 개발뿐 아니라 장애 분석, 고객 문의 대응, 데이터 보정, 배포 관리, 성능 개선, 레거시 코드 이해가 중요하다.
단순히 “운영만 했다”가 아니라 운영 이슈를 코드 구조 개선, 쿼리 튜닝, 모니터링 보강, 배포 프로세스 개선으로 연결한 경험을 설명할 수 있어야 한다.
용어정리 – 아키텍처 및 분산 시스템
아키텍처 경계와 의존성
-
소프트웨어 아키텍처 : 시스템의 주요 구성 요소, 책임, 의존관계와 변경 원칙을 정하는 구조적 결정이다. 좋은 아키텍처는 특정 패턴을 많이 적용한 구조가 아니라, 중요한 변경을 국소화하고 품질 요구사항을 만족하면서 운영 가능한 구조다.
- 응집도와 결합도
- 응집도(Cohesion) : 하나의 모듈 안에 같은 목적과 변경 이유를 가진 기능이 모여 있는 정도다. 관련 규칙과 데이터는 가까이 두는 것이 좋다.
- 결합도(Coupling) : 한 모듈의 변경이 다른 모듈에 영향을 주는 정도다. 결합을 모두 제거할 수는 없으므로 안정적인 인터페이스를 통해 의존 방향과 변경 전파를 통제한다.
- 설계 기준 : 함께 변경되는 코드는 모으고, 서로 다른 이유로 변경되는 코드는 분리한다. 단순히 클래스나 서비스를 많이 나누면 호출과 상태 전달만 늘어날 수 있다.
-
의존성 역전 원칙(DIP) : 상위 수준의 업무 정책이 DB, 메시지 브로커, 외부 API 같은 구체 기술에 직접 의존하지 않고 추상화에 의존하게 하는 원칙이다. 추상화는 사용하는 쪽의 필요에 맞춰 정의하며, 인터페이스를 만들었다는 사실만으로 의존성이 역전되는 것은 아니다.
- 헥사고날 아키텍처(Ports and Adapters) : 애플리케이션 핵심 로직을 외부 환경에서 분리하기 위해 Port와 Adapter를 사용하는 구조다.
- Inbound Port : 외부에서 애플리케이션이 제공하는 기능을 호출하는 진입 계약이다. Use Case 인터페이스가 대표적이다.
- Outbound Port : 애플리케이션이 저장소나 외부 서비스에 요구하는 기능을 정의한 계약이다.
- Adapter : HTTP Controller, JPA Repository 구현, 메시지 Consumer처럼 Port를 외부 기술과 연결한다.
- 의존 방향 : 외부 Adapter가 내부 Port와 업무 규칙에 의존하며, 핵심 로직은 구체적인 프레임워크나 인프라 구현을 알지 않도록 한다.
- 주의점 : 모든 CRUD에 Port와 Adapter를 기계적으로 추가하면 구조만 복잡해질 수 있다. 외부 기술 교체 가능성, 테스트 격리와 업무 규칙의 복잡성이 실제로 경계를 필요로 하는지 판단한다.
-
클린 아키텍처와의 관계 : 클린 아키텍처도 업무 규칙을 중심에 두고 의존성이 안쪽으로 향하게 한다. 헥사고날 아키텍처는 Port와 Adapter를 중심으로 경계를 설명하고, 클린 아키텍처는 Entity·Use Case·Interface Adapter 같은 계층으로 설명하지만 핵심 지향점은 유사하다.
-
Architecture Decision Record(ADR) : 중요한 아키텍처 결정을 배경, 선택지, 결정, 결과와 함께 짧게 기록하는 문서다. “Kafka를 사용한다”보다 해결하려는 문제, 검토한 대안, 감수한 단점과 재검토 조건을 남겨야 이후 변경 근거를 이해할 수 있다.
- 아키텍처 선택 기준 : 패턴 이름보다 변경 빈도, 트랜잭션 경계, 성능·가용성·보안 요구, 팀 구조와 운영 역량을 먼저 본다. 미래 가능성만으로 복잡성을 미리 도입하지 않고, 현재 문제를 해결하는 가장 단순한 구조에서 측정 가능한 필요가 생길 때 확장한다.
MSA와 분산 시스템
-
분산 시스템(Distributed System) : 네트워크로 연결된 여러 프로세스나 서버가 하나의 시스템처럼 협력하는 구조다. 각 노드는 독립적으로 실패할 수 있고, 네트워크 요청은 지연·유실·중복되거나 순서가 바뀔 수 있다. 따라서 원격 호출을 로컬 메서드 호출처럼 취급하면 안 되며 timeout, 재시도, 멱등성, 부분 실패와 관측 가능성을 기본 전제로 설계해야 한다.
-
Monolithic vs MSA : 모놀리식은 하나의 배포 단위에 모든 기능이 포함되어 개발·테스트·운영이 상대적으로 단순하다. 규모가 커지면 변경 영향 범위와 빌드·배포 시간이 증가할 수 있지만, 모듈 경계를 명확히 한 모듈러 모놀리스(Modular Monolith)로 상당 부분 개선할 수 있다. MSA는 기능별 독립 배포와 부분 확장, 장애 격리에 유리하지만 모든 서비스 호출과 데이터 변경에 분산 시스템의 복잡성이 추가된다.
-
MSA (Microservices Architecture) : 애플리케이션을 업무 능력과 데이터 소유권을 기준으로 독립적인 서비스로 나누어 개발·배포하는 아키텍처다. 서비스가 단순히 작거나 API가 많다고 MSA인 것은 아니며, 각 서비스가 자신의 변경과 배포 주기를 독립적으로 가져갈 수 있어야 한다.
- 장점 : 서비스별 독립 배포/확장, 장애 격리, 기술 스택의 다양성(Polyglot), 팀별 독립 개발
- 단점 : 네트워크 지연과 부분 실패, 데이터 정합성 보장의 어려움, 계약·버전 관리, 통합 테스트와 운영 복잡도, 인프라 비용
도입 기준과 서비스 경계
-
MSA 도입 판단 기준 : 조직과 시스템에 독립 배포·확장이 필요한 명확한 경계가 있고, 이를 담당할 팀과 자동화된 배포·관측 인프라가 준비됐을 때 효과가 크다. 팀 규모가 작거나 도메인 경계가 불명확하고 배포 빈도가 낮다면 모듈러 모놀리스가 더 단순하고 경제적일 수 있다. MSA는 성능 개선 기법이 아니라 조직과 변경의 독립성을 얻기 위해 운영 복잡성을 감수하는 선택이다.
- 서비스 경계(Service Boundary) : 기술 레이어나 테이블 수보다 업무 능력과 변경 이유를 기준으로 나눈다. DDD의 Bounded Context를 참고할 수 있으며, 주문·결제처럼 용어와 규칙, 데이터 생명주기가 다른 영역을 분리한다.
- 경계가 너무 크면 독립 배포와 확장의 이점이 줄어든다.
- 경계가 너무 작으면 서비스 간 호출과 배포 단위가 늘어 분산 모놀리스(Distributed Monolith)가 되기 쉽다.
- 하나의 업무 변경이 항상 여러 서비스를 동시에 수정·배포하게 만든다면 서비스 경계를 다시 검토한다.
- 분산 모놀리스 : 서비스는 여러 개로 나뉘었지만 강한 동기 호출, 공유 DB, 공동 배포와 순환 의존 때문에 독립적으로 변경할 수 없는 구조다. 모놀리스의 결합도와 분산 시스템의 운영 비용을 동시에 가지므로 API 계약, 데이터 소유권과 배포 독립성을 점검해야 한다.
통신과 데이터 소유권
- 동기 통신과 비동기 통신
- 동기 통신(HTTP/gRPC) : 호출 결과가 즉시 필요하고 성공·실패를 바로 판단해야 할 때 적합하다. 흐름은 이해하기 쉽지만 호출 체인이 길어질수록 지연 시간과 장애 전파 가능성이 커진다.
- 비동기 통신(Message/Event) : 발행자와 소비자의 시간적 결합을 줄이고 트래픽을 완충할 수 있다. 대신 처리 지연, 중복, 순서, 재처리와 최종적 일관성을 다뤄야 한다.
- 선택 기준 : 사용자 요청 안에서 결과가 반드시 필요하면 동기 호출을 사용하고, 후속 처리나 지연을 허용할 수 있는 작업은 비동기 이벤트를 검토한다. 모든 통신을 한 방식으로 통일할 필요는 없다.
- Database per Service와 데이터 소유권 : 각 서비스는 자신의 데이터를 소유하고 다른 서비스가 해당 저장소를 직접 수정하지 못하게 하는 것이 기본 원칙이다. 반드시 물리 DB 서버를 서비스마다 하나씩 둔다는 뜻은 아니며, 스키마·계정·접근 권한 등으로 논리적 소유권을 분리할 수도 있다.
- 다른 서비스의 데이터는 공개 API, 이벤트 또는 별도 조회 모델을 통해 사용한다.
- 여러 서비스가 같은 테이블을 직접 읽고 쓰면 스키마 변경과 배포가 결합되고 데이터 책임이 불명확해진다.
- 서비스 간 조인과 트랜잭션이 어려워지므로 데이터 중복과 최종적 일관성을 의도적으로 받아들여야 할 수 있다.
-
API 계약과 하위 호환성 : 제공자는 기존 소비자가 사용하는 필드와 의미를 갑자기 제거하거나 변경하지 않는다. 필드 추가 같은 호환 가능한 변경을 먼저 배포하고 소비자 전환 후 이전 계약을 제거하는 방식으로 진화시킨다. Consumer-Driven Contract Test로 실제 소비자의 기대를 자동 검증할 수 있다.
- 멱등성과 요청 식별자 : 네트워크 timeout 뒤 재시도하면 원래 요청과 재시도가 모두 처리될 수 있다. 결제·주문 생성 같은 명령은 idempotency key나 업무 키로 중복 실행을 막고, 이벤트 소비자도 처리한 event ID를 기록하는 등 중복 전달을 전제로 설계한다.
운영과 검증
- MSA 핵심 구성요소
- API Gateway : 외부 클라이언트의 단일 진입점으로 라우팅, 인증, Rate Limiting 같은 공통 관심사를 처리한다. 업무 로직을 과도하게 넣으면 Gateway가 병목과 단일 변경 지점이 될 수 있다. (Spring Cloud Gateway)
- Service Discovery : 동적으로 변하는 서비스 인스턴스의 위치를 등록·조회하는 메커니즘이다. Eureka·Consul 같은 별도 레지스트리나 Kubernetes Service와 DNS를 사용할 수 있다.
- 중앙 설정 관리 : 환경별 설정을 일관되게 배포하고 변경 이력을 관리한다. 비밀값은 일반 Config Server보다 Vault나 클라우드 Secret Manager 같은 전용 저장소로 분리한다.
- 분산 추적(Distributed Tracing) : 여러 서비스를 거치는 요청에 Trace ID와 Span ID를 전파해 호출 흐름과 지연 구간을 추적한다. 로그·메트릭과 연결해야 장애 원인을 좁힐 수 있다.
-
장애 전파와 호출 체인 : 서비스 A가 B를, B가 C를 동기 호출하면 C의 지연이 전체 요청과 스레드·커넥션 풀 고갈로 전파될 수 있다. 각 호출에 timeout을 두고, 재시도는 일시적이고 멱등한 실패에만 제한적으로 적용한다. Circuit Breaker와 Bulkhead는 뒤의 장애 대응 패턴과 함께 설계한다.
-
배포 독립성과 호환 순서 : 서비스가 나뉘어 있어도 항상 같은 순서로 함께 배포해야 한다면 독립성이 약하다. DB와 API 변경은
확장(새 구조 추가) → 새 구조 사용 → 이전 구조 제거순서로 진행하고, 신·구 버전이 동시에 동작하는 배포 구간을 고려한다. -
MSA 테스트 전략 : 서비스 내부 로직은 단위·통합 테스트로 빠르게 검증하고, 서비스 간 API·이벤트 스키마는 계약 테스트로 확인한다. 핵심 사용자 흐름만 제한된 End-to-End 테스트로 검증하며, 네트워크 지연·timeout·중복 메시지·의존 서비스 장애 같은 실패 시나리오도 포함한다.
- 운영 준비도 : 중앙 로그, 메트릭, 분산 추적, 표준화된 health check, 자동 배포·롤백과 서비스별 SLO가 없으면 서비스 수만큼 장애 분석 비용이 증가한다. 각 서비스의 소유 팀과 온콜·복구 책임도 명확해야 한다.
분산 트랜잭션과 일관성
- CAP 이론 : 네트워크 분할이 발생한 동안 Consistency(일관성)와 Availability(가용성)를 동시에 완전히 보장할 수 없다는 이론이다. 분산 시스템에서 네트워크 분할 가능성을 무시하기 어려우므로, 분할 시 요청을 거부·대기해 일관성을 지킬지(CP), 일부 노드의 오래된 응답을 허용해 가용성을 지킬지(AP)를 결정해야 한다.
- CP 시스템 성향 : 일관성을 위해 일부 요청을 거부하거나 대기 (예: ZooKeeper, etcd, HBase)
- AP 시스템 성향 : 가용성을 위해 오래된 데이터라도 응답 (예: Cassandra, DynamoDB)
- 주의점 : 실제 제품을 단순히 CP/AP로 고정 분류하기는 어렵다. 설정, 쿼럼, 복제 방식, 읽기/쓰기 옵션에 따라 일관성과 가용성의 균형이 달라진다.
-
PACELC 이론 : CAP 이론의 확장으로, 네트워크 분할(P)이 있을 때는 A와 C를 트레이드오프하고,
분할이 없을 때(Else)는 지연 시간(Latency)과 일관성(Consistency) 사이의 트레이드오프가 존재함을 설명한다. - 일관성 모델
- 강한 일관성(Strong Consistency) : 성공한 쓰기 이후의 읽기가 최신 값을 반환하도록 보장하는 모델이다. 사용하기 단순하지만 노드 간 조정으로 지연이나 가용성 비용이 커질 수 있다.
- 최종적 일관성(Eventual Consistency) : 새 변경이 없는 상태가 지속되면 시간이 지나 모든 복제본이나 조회 모델이 같은 상태로 수렴하는 모델이다. 수렴 전 오래된 값이 보일 수 있으므로 사용자 경험과 업무 규칙이 이를 허용하는지 판단해야 한다.
- 인과적 일관성(Causal Consistency) : 원인과 결과 관계가 있는 작업의 순서를 보장하는 모델이다. 모든 작업의 전역 순서보다 약하지만 연관된 변경의 관찰 순서를 유지한다.
MSA에서 정합성을 유지하는 방법
-
정합성의 기본 원칙 : MSA에서 모든 데이터를 하나의 분산 트랜잭션으로 묶는 것이 정답은 아니다. 반드시 동시에 확정돼야 하는 규칙은 한 서비스와 로컬 트랜잭션 안에 두고, 서비스 경계를 넘는 상태 변화는 이벤트, 멱등성, 보상과 대사 작업을 조합해 수렴시킨다.
-
1. 정합성 경계를 서비스 안으로 모은다 : 하나의 업무 불변식(Invariant)을 지키는 데이터는 가능하면 같은 서비스가 소유하고 하나의 DB 트랜잭션으로 변경한다. 예를 들어 재고 수량이 음수가 되면 안 된다는 규칙은 재고 서비스 내부에서 조건부 UPDATE나 락으로 원자적으로 보장한다. 서비스마다 데이터를 나눴다는 이유로 하나의 규칙까지 여러 서비스에 분산하면 정합성 유지가 훨씬 어려워진다.
-
2. 데이터의 단일 소유자를 정한다 : 주문 상태는 주문 서비스, 결제 승인 상태는 결제 서비스처럼 각 데이터의 최종 판단 주체를 하나로 둔다. 다른 서비스는 소유 DB를 직접 수정하지 않고 API나 이벤트를 통해 변경을 요청한다. 여러 서비스가 같은 값을 각각 원본으로 관리하면 어느 값이 맞는지 결정할 수 없다.
-
3. 상태 변경과 이벤트 기록을 원자적으로 처리한다 : 업무 데이터를 커밋한 뒤 별도로 메시지를 발행하면 둘 중 하나만 성공하는 Dual Write 문제가 생긴다. Transactional Outbox를 사용해 업무 변경과 발행할 이벤트를 같은 로컬 트랜잭션에 저장하고, 별도 릴레이가 이벤트를 브로커로 전달하게 한다.
-
4. 메시지 중복을 전제로 소비한다 : 메시지 브로커와 Outbox 릴레이는 장애 복구 과정에서 같은 이벤트를 다시 전달할 수 있다. 소비자는 event ID를 Inbox나 처리 이력에 기록하고, 업무 변경과 중복 처리 기록을 같은 로컬 트랜잭션으로 커밋한다. 단순 조회 후 INSERT보다 Unique Key, 조건부 UPDATE, upsert 같은 DB 제약을 함께 사용해야 동시 중복 처리도 막을 수 있다.
-
5. 여러 서비스의 업무 흐름은 Saga로 관리한다 : 주문·결제·재고처럼 여러 로컬 트랜잭션이 순서대로 실행되는 작업은 Saga 상태를 명시하고 각 단계의 성공·실패·보상 이벤트를 관리한다. 중앙 오케스트레이터를 사용하면 전체 흐름과 timeout을 파악하기 쉽고, Choreography를 사용하면 중앙 의존성은 줄지만 이벤트 흐름 추적이 어려워질 수 있다.
-
6. 순서 역전과 동시 수정을 처리한다 : 같은 Aggregate의 이벤트는 동일한 파티션 키를 사용해 순서를 유지하고, 이벤트에 aggregate ID와 version을 포함한다. 소비자는 현재 version보다 오래된 이벤트를 무시하거나 보류하며, HTTP 기반 수정은 version 컬럼·ETag·조건부 UPDATE로 Lost Update를 방지한다. 네트워크 전체에서 완벽한 전역 순서를 만들기보다 업무상 순서가 필요한 단위를 명확히 정한다.
-
7. 중간 상태를 숨기지 않고 모델링한다 : 최종적 일관성을 사용하면
주문 생성 완료직후 결제나 재고가 아직 확정되지 않을 수 있다. 이를 오류처럼 숨기지 말고PAYMENT_PENDING,STOCK_RESERVING,CONFIRMED,FAILED같은 상태로 표현한다. API는 처리 중 상태와 재조회 방법을 제공하고, timeout 이후에도 백그라운드 처리가 완료될 수 있음을 고려한다. -
8. 대사와 수동 복구를 마지막 안전망으로 둔다 : 네트워크 장애, 운영자 개입, 외부 결제 시스템 오류까지 모든 경우를 실시간 로직만으로 해결하기는 어렵다. 주기적으로 주문 금액과 결제 승인, 예약 재고를 비교하는 대사 작업을 실행하고, 불일치를 재처리하거나 운영자가 감사 로그를 보고 보정할 수 있게 한다.
-
상황별 선택 기준
상황 우선 검토할 방법 주의점 한 서비스·한 DB 안의 변경 로컬 트랜잭션, DB 제약, 조건부 UPDATE 불필요하게 이벤트로 분리하지 않는다. DB 변경 후 이벤트 발행 Transactional Outbox 중복 발행 가능성이 있어 소비자가 멱등해야 한다. 여러 서비스의 단계적 업무 처리 Saga와 보상 트랜잭션 중간 상태, timeout, 보상 실패를 모델링한다. 자원의 임시 확보가 필요한 처리 TCC 또는 예약 상태 만료와 Confirm·Cancel 재시도를 설계한다. 중복 메시지 처리 Inbox, event ID, Unique Key, upsert 조회 후 저장만으로는 동시 실행을 막기 어렵다. 순서가 중요한 상태 변경 Aggregate별 key와 version 모든 이벤트의 전역 순서를 요구하지 않는다. 시스템 간 장기 불일치 탐지 대사 작업과 수동 복구 도구 재처리 이력과 감사 로그를 남긴다. - 주문·결제·재고 예시
- 주문 서비스가 주문을
PENDING으로 저장하면서 Outbox에OrderCreated이벤트를 같은 트랜잭션으로 기록한다. - 재고 서비스는 이벤트 ID 중복을 확인하고 재고를 조건부로 예약한 뒤
StockReserved또는StockReservationFailed를 발행한다. - 결제 서비스는 재고 예약 성공 후 결제를 승인하고
PaymentApproved또는PaymentFailed를 발행한다. - 주문 서비스는 두 결과가 성공하면
CONFIRMED로 변경한다. 결제가 실패하면 재고 예약 취소를 요청하고 주문을FAILED로 변경한다. - 이벤트가 중복되거나 순서가 늦어져도 각 서비스는 event ID와 업무 상태 전이 규칙으로 같은 부수 효과가 반복되지 않게 한다.
- 일정 시간 이상
PENDING인 주문은 timeout 처리하고, 대사 작업이 주문·결제·재고 상태를 비교해 누락된 이벤트를 재처리하거나 운영자에게 알린다.
- 주문 서비스가 주문을
-
핵심 판단 질문 : “모든 데이터가 항상 같은가?”보다 “어떤 규칙을 즉시 지켜야 하는가, 어느 정도 지연을 허용하는가, 실패하면 누가 어떤 상태로 복구하는가?”를 먼저 정의해야 한다. MSA의 정합성은 단일 기술이 아니라 로컬 트랜잭션, 데이터 소유권, 이벤트 전달, 멱등성, 보상과 운영 복구를 연결한 결과다.
- 분산 트랜잭션과 데이터 정합성
- 2PC (Two-Phase Commit) : 코디네이터가 Prepare(준비) → Commit(확정) 두 단계로 모든 참여 노드의 커밋을 동기적으로 보장.
원자적 커밋을 제공하지만 장애 시 블로킹 가능성과 참여자의 자원·락 유지로 성능과 가용성 비용이 크다. 참여 시스템과 트랜잭션 매니저가 지원하고 강한 원자성이 반드시 필요한 제한된 범위에서 검토한다. - Saga 패턴 : MSA 환경에서 여러 서비스에 걸친 분산 트랜잭션을 관리하는 패턴. 각 서비스의 로컬 트랜잭션을 순차적으로 실행하며,
중간에 실패하면 보상 트랜잭션(Compensating Transaction)으로 이전 작업을 되돌려 데이터 일관성(최종적 일관성)을 맞춘다.- Choreography(코레오그래피) : 중앙 조정자 없이 각 서비스가 이벤트를 발행/구독하며 다음 단계를 진행.
구현이 단순하지만 흐름 파악이 어렵고 서비스 간 결합이 생길 수 있다. - Orchestration(오케스트레이션) : 중앙 오케스트레이터가 각 서비스를 순서대로 호출하고 보상 로직을 관리.
흐름이 명확하지만 오케스트레이터에 로직이 집중된다.
- Choreography(코레오그래피) : 중앙 조정자 없이 각 서비스가 이벤트를 발행/구독하며 다음 단계를 진행.
- TCC(Try-Confirm-Cancel) : 각 참여 서비스가 자원을 임시 확보(Try)한 뒤 모두 성공하면 확정(Confirm), 실패하면 취소(Cancel)하는 방식이다. 예약·재고 확보처럼 명시적인 보류 상태를 만들 수 있는 업무에 적합하지만 각 단계의 API와 만료·복구 로직을 별도로 구현해야 한다.
- 2PC (Two-Phase Commit) : 코디네이터가 Prepare(준비) → Commit(확정) 두 단계로 모든 참여 노드의 커밋을 동기적으로 보장.
-
분산 트랜잭션 처리 판단 기준 : 2PC는 강한 일관성을 제공하지만 락 유지, 코디네이터 장애, 가용성 저하 부담이 크다.
일반적인 MSA에서는 각 서비스의 로컬 트랜잭션과 이벤트를 조합하고, 실패 시 보상 트랜잭션을 수행하는 Saga 패턴을 더 자주 사용한다. DB 변경과 이벤트 발행의 이중 쓰기 문제는 Transactional Outbox로 줄인다. -
Saga 보상의 한계 : 보상 트랜잭션은 DB rollback처럼 시간을 되돌리는 기능이 아니라 이미 완료된 상태를 새 작업으로 상쇄하는 것이다. 이미 완료된 외부 작업처럼 완전히 되돌릴 수 없는 단계가 있으므로, 각 단계의 멱등성·재시도·수동 복구 절차와 중간 상태의 사용자 노출 방식을 설계해야 한다.
-
일관성 경계의 명시 : 어떤 데이터가 즉시 일관되어야 하는지와, 이벤트 전달 지연 동안 최종 일관성을 허용할 수 있는지를 구분한다. 즉시 일관성이 필요한 변경은 조건부 갱신·트랜잭션으로 보호하고, 지연을 허용할 수 있는 작업은 이벤트 기반 비동기 처리를 검토한다. 설계의 핵심은 사용 기술의 이름보다 실패 시 허용되는 상태와 복구 절차를 정하는 것이다.
- 정합성 검증과 복구 : 재시도와 보상만으로 모든 불일치를 막을 수는 없다. 주문·결제·재고처럼 중요한 데이터는 주기적인 대사(Reconciliation) 작업으로 시스템 간 상태를 비교하고, 자동 보정이 위험한 경우 운영자가 원인과 이력을 확인해 재처리할 수 있는 관리 도구와 감사 로그를 마련한다.
이벤트 기반 설계
-
Event-Driven Architecture(EDA) : 서비스가 상태 변경을 이벤트로 발행하고 다른 서비스가 이를 구독해 후속 작업을 수행하는 구조다. 발행자는 소비자를 직접 알 필요가 없어 결합도를 낮출 수 있지만, 이벤트 계약·중복·순서·지연·재처리와 전체 흐름 추적이 새로운 설계 과제가 된다.
- 이벤트와 명령의 구분
- Command(명령) : 특정 수신자에게 작업 수행을 요청하며
주문을 취소하라처럼 의도가 담긴다. 거절되거나 실패할 수 있다. - Event(이벤트) : 이미 발생한 사실을
주문이 취소됐다처럼 과거형으로 표현한다. 발행자는 소비자가 어떤 후속 작업을 할지 가정하지 않는 것이 좋다. - 명령을 이벤트처럼 방송하면 책임이 불명확해지고, 이벤트에 특정 소비자의 처리 절차를 넣으면 서비스 간 결합이 커진다.
- Command(명령) : 특정 수신자에게 작업 수행을 요청하며
-
이벤트 계약(Event Contract) : 이벤트 이름, 의미, 식별자, 발생 시각, 집계 ID, 스키마 버전을 명확히 정의한다. 기존 소비자를 깨지 않도록 필드 추가 중심으로 진화시키고, 의미 변경이 크면 새 이벤트 타입이나 버전을 사용한다. 이벤트는 발행 서비스의 내부 테이블 구조를 그대로 노출하지 않는다.
-
CQRS (Command Query Responsibility Segregation) : 상태를 변경하는 명령 모델과 조회 모델을 분리하는 패턴이다. 같은 DB에서 코드 모델만 나눌 수도 있고, 필요하면 별도 읽기 저장소와 비정규화된 뷰를 둘 수도 있다. 읽기·쓰기 요구가 크게 다를 때 유용하지만 동기화 지연과 운영 대상이 증가하므로 단순 CRUD에는 과할 수 있다.
-
Event Sourcing : 현재 상태만 저장하지 않고 상태를 변경시킨 이벤트를 순서대로 저장하고 재생해 상태를 복원하는 방식이다. 감사와 시점 복원에 유리하지만 이벤트 불변성, 스키마 진화, 재생 비용과 외부 부수 효과를 다루기 어렵다. CQRS와 함께 사용할 수 있지만 두 패턴이 서로 필수인 것은 아니다.
- Transactional Outbox 패턴 : DB 업데이트와 메시지 발행을 하나의 로컬 트랜잭션으로 묶어 처리하는 패턴.
비즈니스 데이터와 함께 Outbox 테이블에 메시지를 저장하고, 별도의 릴레이 프로세스(또는 CDC)가 Outbox 테이블을 읽어 메시지 브로커로 전달한다. “DB 커밋은 됐는데 메시지 발행은 실패”하는 이중 쓰기(Dual Write) 문제를 줄이지만, 릴레이 재시도로 메시지가 중복 발행될 수 있으므로 소비자 멱등성이 필요하다.
- CDC (Change Data Capture) : DB의 변경 로그(예: MySQL binlog)를 캡처해 이벤트로 전파하는 기법 (Debezium)
-
Inbox 패턴 : 소비자가 수신한 메시지 ID와 처리 상태를 자신의 DB에 기록해 중복 처리를 막는 방식이다. 업무 변경과 Inbox 기록을 같은 로컬 트랜잭션으로 처리하고, 실패 메시지는 재시도·DLQ·수동 복구 정책에 따라 관리한다.
- 이벤트 처리 운영 기준 : 이벤트 지연 시간, 소비 지연(lag), 실패·재시도 횟수와 DLQ 적재량을 관측한다. 재처리는 과거 이벤트가 현재 로직으로 다시 실행되는 작업이므로 순서, 중복 부수 효과와 스키마 호환성을 확인하고 실행 주체·범위·감사 기록을 남긴다.
장애 대응 패턴
-
Resilience(회복 탄력성) : 장애를 완전히 없애는 것이 아니라 일부 구성 요소가 실패해도 영향 범위를 제한하고, 시스템이 예측 가능한 방식으로 기능을 유지·복구하게 하는 능력이다. 패턴 하나로 달성되는 속성이 아니며 용량 계획, 격리, 관측, 복구 절차와 함께 설계한다.
-
Timeout과 Deadline : 모든 원격 호출에는 연결·응답 timeout을 설정한다. 상위 요청의 전체 deadline보다 하위 호출 timeout의 합이 길면 이미 응답할 수 없는 작업이 계속 자원을 소비한다. 남은 시간 예산을 하위 호출에 전달하고 취소가 가능한 작업은 deadline 초과 시 중단한다.
-
Retry(재시도) : 일시적인 네트워크 오류나 과부하처럼 다시 성공할 가능성이 있는 실패에만 적용한다. 지수 백오프와 jitter, 최대 횟수를 두고 멱등하지 않은 작업은 중복 실행 방지 장치 없이 재시도하지 않는다. 여러 계층에서 동시에 재시도하면 호출 수가 기하급수적으로 늘어나는 Retry Storm이 발생할 수 있으므로 재시도 책임을 한 계층에 둔다.
-
Circuit Breaker(서킷 브레이커) : 일정 구간의 실패율이나 지연이 임계치를 넘으면 회로를 열어 호출을 빠르게 실패시키고, 장애 시스템과 호출자의 자원을 보호한다. 일정 시간이 지나면 제한된 요청으로 회복 여부를 확인한다.
Closed → Open → Half-Open상태로 동작하며 임계값은 실제 트래픽과 SLO를 기준으로 조정한다. (Resilience4j) -
Bulkhead(벌크헤드) : 의존 서비스나 작업 유형별로 스레드 풀, 커넥션, 큐와 동시 실행 수를 분리해 한 영역의 지연이 전체 자원을 고갈시키지 않도록 한다. 격리만 하고 큐를 무제한으로 두면 지연이 누적되므로 큐 크기와 거부 정책도 함께 정한다.
-
Fallback과 Graceful Degradation : 핵심 기능이 실패했을 때 캐시된 값, 축소된 응답, 비핵심 기능 생략으로 제한적인 서비스를 제공하는 방식이다. 오래된 데이터가 잘못된 의사결정을 만들 수 있는 결제·권한·재고에는 무조건 fallback하지 않고 명확히 실패시키는 편이 안전할 수 있다.
-
Load Shedding(부하 차단) : 시스템 용량을 넘는 요청을 조기에 거부해 이미 처리 중인 요청과 핵심 기능을 보호하는 방식이다. 우선순위, Rate Limit, 큐 상한과
429 Too Many Requests또는503 Service Unavailable응답 및 재시도 힌트를 함께 설계한다. -
Backpressure(배압) : 생산 속도가 소비 속도보다 빠를 때 소비자가 처리 가능한 양만 받거나 생산 속도를 늦추게 하는 흐름 제어다. 메시지 소비 동시성, prefetch, bounded queue, Reactive Streams의 demand 등을 이용해 메모리와 지연의 무제한 증가를 막는다.
-
복구와 검증 : 장애가 끝났다고 데이터가 자동으로 정상화되는 것은 아니다. 재처리·대사·수동 보정 절차와 Runbook을 준비하고, Chaos Engineering이나 장애 주입 테스트로 timeout, 격리, 자동 복구와 알림이 실제로 동작하는지 제한된 환경부터 검증한다.
멀티테넌시
- 테넌트(Tenant) : 하나의 애플리케이션을 독립적으로 사용하는 고객 조직이나 계정을 뜻한다. SaaS 서비스에서는 회사, 학교, 병원, 쇼핑몰 입점사처럼 데이터·사용자·권한·설정을 다른 고객과 분리해야 하는 단위가 테넌트가 된다.
- 예를 들어 협업 SaaS를 A회사와 B회사가 함께 사용한다면 A회사와 B회사가 각각 하나의 테넌트다.
- 한 테넌트에는 여러 사용자가 속할 수 있으며, 같은 사용자가 여러 테넌트에 소속될 수도 있다.
- 테넌트는 반드시 회사만 의미하지 않는다. 서비스 정책에 따라 개인 계정, 프로젝트, 워크스페이스나 판매자가 격리 단위가 될 수 있다.
- 시스템은 현재 요청이 어느 테넌트에 속하는지 식별하고, 해당 테넌트가 소유한 데이터와 기능에만 접근하도록 제한해야 한다.
-
멀티테넌시(Multi-tenancy) : 하나의 애플리케이션을 여러 고객 조직(테넌트)이 함께 사용하되, 각 테넌트의 데이터·권한·설정을 분리하는 SaaS 설계 방식이다.
- 멀티테넌시와 멀티테넌트의 차이
- 멀티테넌시(Multi-tenancy) : 여러 테넌트를 지원하는 아키텍처나 운영 방식을 뜻하는 명사다. 예:
멀티테넌시 아키텍처,멀티테넌시 전략 - 멀티테넌트(Multi-tenant) : 여러 테넌트가 사용하는 시스템의 특성을 나타낸다. 예:
멀티테넌트 애플리케이션,멀티테넌트 환경 - 이 문서에서는 설계 방식과 선택 기준에는
멀티테넌시, 시스템이나 환경의 특성을 설명할 때는멀티테넌트를 사용한다.
- 멀티테넌시(Multi-tenancy) : 여러 테넌트를 지원하는 아키텍처나 운영 방식을 뜻하는 명사다. 예:
-
테넌트 컨텍스트(Tenant Context) : 현재 요청이나 작업이 어느 테넌트에 속하는지를 나타내는 정보다. 로그인 토큰·세션처럼 서버가 검증한 인증 정보에서 테넌트 ID를 확정하고 서비스, Repository, 비동기 작업과 메시지까지 전달한다. 사용자가 임의로 보낸 요청값만으로 테넌트를 결정하면 안 된다.
- 테넌트 격리 모델
- 공유 DB·공유 스키마 : 모든 테넌트가 같은 DB와 테이블을 사용하고, 각 행에
tenant_id를 저장해 소유 테넌트를 구분하는 행 단위 격리 방식이다. 비용과 운영 부담은 낮지만 모든 조회·수정·삭제와 Unique Key에 테넌트 범위를 포함하고 교차 접근을 구조적으로 차단해야 한다. - 공유 DB·테넌트별 스키마 : DB 인스턴스는 공유하고 스키마를 분리한다. 데이터 경계와 테넌트별 마이그레이션은 명확하지만 스키마 수가 많아지면 배포와 커넥션 관리가 복잡해진다.
- 테넌트별 DB : 데이터베이스를 완전히 분리해 격리, 백업·복구와 규제 대응에 유리하다. 대신 인프라 비용과 연결, 마이그레이션, 통합 조회의 운영 부담이 가장 크다.
- 혼합 모델(Hybrid) : 일반 테넌트는 공유하고 대규모·규제 대상 테넌트만 전용 DB로 분리한다. 테넌트를 다른 저장소로 이동할 수 있는 라우팅과 마이그레이션 절차가 필요하다.
- 공유 DB·공유 스키마 : 모든 테넌트가 같은 DB와 테이블을 사용하고, 각 행에
-
공유 DB·공유 스키마 예시 : 고객사 A와 B의 주문을 별도 테이블로 만들지 않고 하나의
orders테이블에 함께 저장한다.tenant_id = 100은 고객사 A,tenant_id = 200은 고객사 B처럼 각 행의 소유자를 표시한다.CREATE TABLE orders ( id BIGINT NOT NULL, tenant_id BIGINT NOT NULL, order_no VARCHAR(50) NOT NULL, amount DECIMAL(15, 2) NOT NULL, PRIMARY KEY (id), UNIQUE (tenant_id, order_no) ); SELECT id, order_no, amount FROM orders WHERE tenant_id = :authenticatedTenantId AND id = :orderId;order_no는 테넌트마다 같은 값을 사용할 수 있으므로(tenant_id, order_no)를 하나의 Unique Key로 묶는다. 조회뿐 아니라UPDATE,DELETE에도tenant_id조건이 필요하며, 자주 사용하는 조회 조건에 맞춰(tenant_id, ...)복합 인덱스를 설계한다. -
테넌트 ID 적용 시 주의점 : 클라이언트가 요청 파라미터나 헤더로 보낸
tenant_id를 그대로 신뢰하면 다른 테넌트 ID로 바꿔 호출할 수 있다. 로그인 토큰·세션처럼 서버가 검증한 인증 정보에서 테넌트 ID를 확정하고 Repository까지 전달해야 한다. 공통 쿼리 필터나 PostgreSQL Row-Level Security 같은 DB 기능을 보조 방어선으로 사용할 수 있지만, 배치·Native Query·관리자 기능까지 누락 없이 적용되는지 테스트해야 한다. -
테넌트 격리의 적용 범위 : DB뿐 아니라 캐시 키, 파일 경로, 메시지, 검색 인덱스, 로그, 메트릭과 백업에도 테넌트 식별자와 접근 정책을 적용한다. 관리자 기능과 배치 작업도 테넌트 범위를 명시하지 않으면 대량 교차 노출 경로가 될 수 있다.
-
Noisy Neighbor 문제 : 한 테넌트의 과도한 트래픽이나 대용량 쿼리가 공유 CPU, 메모리, 커넥션 풀을 점유해 다른 테넌트의 품질을 떨어뜨리는 현상이다. 테넌트별 Rate Limit, 동시 실행 제한, 큐, 쿼터와 사용량 측정으로 공정성을 유지한다.
-
테넌트별 설정과 기능 : 요금제, 기능 플래그, 로케일, 데이터 보존 기간처럼 테넌트별로 달라지는 설정은 코드 분기보다 명시적인 설정 모델로 관리한다. 설정 변경의 권한, 감사 로그, 캐시 무효화와 기본값을 정의한다.
-
마이그레이션과 운영 : 스키마가 여러 개거나 테넌트별 DB를 사용하면 일부 테넌트만 마이그레이션에 실패할 수 있다. 적용 버전을 테넌트별로 추적하고 재시도·롤백·격리 절차를 마련하며, 백업 복구도 특정 테넌트 단위로 가능한지 확인한다.
- 멀티테넌시 아키텍처 선택 기준 : 비용만 비교하지 않고 데이터 민감도, 규제, 테넌트 수와 크기 편차, 커스터마이징 요구, 장애 영향 범위와 복구 단위를 함께 고려한다. 초기에는 공유 모델로 시작하더라도 대형 테넌트를 분리할 가능성이 있다면 데이터 이동과 라우팅 경계를 미리 명확히 한다.
용어정리 – 메시지 브로커 및 이벤트 기반
Kafka
- 메시지 브로커(Message Broker) : 시스템 간 메시지를 비동기적으로 중계해주는 미들웨어.
생산자(Producer)와 소비자(Consumer)를 분리하여 결합도를 낮추고(Decoupling), 일시적 부하를 완충(Buffering)하며, 트래픽 급증을 받아내는 역할을 한다.- 동기 통신 vs 비동기 메시징 : REST 같은 동기 호출은 응답을 기다리며 강하게 결합되지만,
메시징은 발행 후 즉시 반환되어 느슨하게 결합되고 장애 격리에 유리하다.
- 동기 통신 vs 비동기 메시징 : REST 같은 동기 호출은 응답을 기다리며 강하게 결합되지만,
-
Kafka (Apache Kafka) : 대용량 실시간 데이터 스트리밍을 위한 분산 이벤트 스트리밍 플랫폼.
메시지를 디스크에 로그(Append-only Log) 형태로 영구 저장하고, 높은 처리량(High Throughput)과 수평 확장에 강점이 있다. 단순 큐를 넘어 이벤트 소싱/로그 수집/스트림 처리에 널리 쓰인다. - Kafka 핵심 개념
- Producer / Consumer : 메시지를 발행하는 주체 / 구독하여 소비하는 주체
- Topic(토픽) : 메시지가 저장되는 논리적 채널(카테고리)
- Partition(파티션) : 토픽을 나눈 물리적 단위. 파티션 수만큼 병렬 처리가 가능해 처리량과 확장성의 핵심이다. 파티션 내에서는 순서가 보장되지만,
토픽 전체의 순서는 보장되지 않는다. - Offset(오프셋) : 파티션 내 각 메시지의 고유한 순번. 소비자는 어디까지 읽었는지를 오프셋으로 관리(커밋)한다.
- Consumer Group(컨슈머 그룹) : 같은 그룹의 컨슈머들이 파티션을 나눠 병렬 소비. 한 파티션은 그룹 내 하나의 컨슈머에만 할당된다.
(컨슈머 수 ≤ 파티션 수일 때 효율적) - Broker / Cluster : 메시지를 저장/전달하는 Kafka 서버 / 그 서버들의 집합
- Replication(복제) : 파티션을 여러 브로커에 복제(Leader-Follower)하여 장애에 대비.
Leader가 읽기/쓰기를 담당하고 장애 시 Follower가 승격된다. - ISR(In-Sync Replicas) : Leader와 동기화 상태를 유지하는 Replica 목록.
acks=all은 ISR에 있는 Replica들이 기록을 확인해야 성공으로 응답한다. - Zookeeper / KRaft : 기존에는 Zookeeper가 메타데이터/리더 선출을 관리했으나, 최신 버전은 이를 제거한 KRaft 모드로 전환되고 있다.
메시징 모델과 브로커 비교
- 메시징 모델
- Point-to-Point (Queue) : 하나의 메시지를 하나의 소비자만 처리. 작업 분산(Work Queue)에 적합.
- Publish-Subscribe (Topic) : 하나의 메시지를 구독한 모든 소비자가 수신. 이벤트 브로드캐스트에 적합.
- Kafka vs RabbitMQ : 둘 다 대표적인 메시지 브로커지만 지향점이 다르다.
- Kafka : 로그 기반, 높은 처리량과 데이터 보존(재처리 가능)에 강점. 대용량 스트리밍/이벤트 소싱에 적합. Consumer가 offset을 관리하며 Pull 방식으로 가져간다.
- RabbitMQ : 전통적인 메시지 큐(AMQP). 복잡한 라우팅과 낮은 지연, 메시지 단위 처리에 강점. 메시지는 consumer ack 이후 큐에서 제거되는 것이 일반적이며, prefetch/QoS로 소비 속도를 제어한다.
- 이벤트 기반 아키텍처(EDA, Event-Driven Architecture) : 서비스 간 상태 변화를 이벤트로 발행/구독하여 통신하는 아키텍처. MSA에서 서비스 간 결합도를 낮추고 비동기 확장성을 확보하는 데 활용되며, 앞서 다룬 Saga, Transactional Outbox, CQRS와 함께 자주 등장한다.
메시지 전달 보장과 이벤트 처리
- 메시지 전달 보장(Delivery Semantics)
- At-Most-Once : 최대 한 번. 유실 가능성은 있으나 중복은 없음 (성능 우선)
- At-Least-Once : 최소 한 번. 유실은 없으나 중복 가능 → 소비자의 멱등성(Idempotency) 처리 필요. (가장 일반적)
- Exactly-Once : 정확히 한 번. Kafka는 멱등 프로듀서와 트랜잭션으로 Kafka 내부의 consume-process-produce 흐름에서 정확히 한 번 처리를 지원할 수 있다. 단, 외부 DB/API까지 포함하면 별도의 멱등 처리나 트랜잭션 아웃박스 같은 패턴이 필요하다.
- Kafka Producer 주요 설정
- acks : 브로커가 메시지를 저장했다고 응답하는 기준.
acks=0은 빠르지만 유실 위험이 크고,acks=all은 ISR 복제까지 기다려 안정성이 높다. - enable.idempotence : 프로듀서 재시도 과정에서 중복 기록을 방지하는 옵션.
- key : 같은 key를 가진 메시지는 같은 파티션으로 전송되어 해당 파티션 안에서 순서가 보장된다.
- acks : 브로커가 메시지를 저장했다고 응답하는 기준.
-
순서 보장 : 순서가 중요한 메시지는 동일한 Key를 사용해 같은 파티션으로 보내면 순서가 보장된다.
-
중복과 순서 처리 기준 : Kafka는 보통 At-Least-Once로 운영하므로 중복 소비를 전제로 처리해야 한다. 메시지 ID로 처리 이력을 저장하거나 upsert를 사용해 재전달된 메시지의 부수 효과를 막는다. 순서가 중요한 이벤트는 같은 key를 사용해 같은 파티션으로 보내야 하며, 순서는 파티션 내부에서만 보장된다.
-
Offset 커밋과 재처리 : DB 저장 같은 부수 효과를 먼저 완료하고 offset을 커밋하면,
커밋 전 장애 시 메시지가 재전달될 수 있으므로 소비자는 멱등해야 한다. 반대로 offset을 먼저 커밋하면 처리 전 장애에서 메시지가 유실될 수 있다. 실패한 이벤트를 재처리할 때는 원본 토픽을 무작정 되감기보다, 처리 이력·재처리 범위·순서 영향·DLQ 복구 절차를 정하고 별도 consumer group 또는 재처리 토픽을 사용하는 편이 안전하다. -
파티션 수와 순서의 트레이드오프 : 같은 key는 한 파티션에서 순서가 보장되지만, 특정 key에 트래픽이 몰리면 해당 파티션 하나가 병목이 된다.
파티션을 늘리면 병렬성은 증가하지만 key의 파티션 매핑이 바뀔 수 있어, 기존·신규 이벤트 사이의 전역 순서를 기대해서는 안 된다. 순서가 필요한 범위를 최소 단위로 정하고, 순서가 필요 없는 이벤트는 분산 가능한 key를 사용한다. - DLQ (Dead Letter Queue) : 정해진 횟수만큼 재시도해도 처리에 실패한 메시지를 별도 큐로 보내 격리/분석하는 메커니즘.
실패 메시지가 전체 처리를 막는 것을 방지한다.
용어정리 – 애플리케이션 설계와 품질
API 설계와 HTTP
-
REST API : 자원(Resource)을 URI로 식별하고, HTTP 메서드로 자원에 대한 행위를 표현하는 API 설계 스타일.
핵심은 URI를 동사보다 명사 중심으로 잡고, 행위는 GET/POST/PUT/PATCH/DELETE 같은 메서드 의미에 맡기는 것이다. - REST API 주요 요소
- 자원(Resource) :
/users,/orders/1처럼 URI로 식별되는 대상 - 표현(Representation) : JSON, XML, HTML 등 클라이언트와 서버가 주고받는 자원의 형태
- 무상태성(Stateless) : 서버가 요청 간 클라이언트 상태를 저장하지 않고, 각 요청이 필요한 정보를 포함해야 한다.
- HATEOAS : 응답에 다음 행동 링크를 포함해 클라이언트가 상태 전이를 따라가게 하는 제약. 실무 API에서는 필수로 적용하지 않는 경우도 많다.
- 자원(Resource) :
- HTTP 메서드 선택 기준
- GET : 조회 (
GET /users/1) - POST : 생성 또는 명령성 처리 (
POST /users,POST /orders/1/cancel) - PUT : 자원 전체 교체. 같은 요청을 여러 번 보내도 결과가 같아야 한다.
- PATCH : 자원 일부 변경. 변경 방식에 따라 멱등하지 않을 수 있다.
- DELETE : 삭제. 여러 번 호출해도 최종 상태는 동일하게 삭제된 상태다.
- GET : 조회 (
- URI 설계 기준
- 명사 사용 :
/getUser보다/users/{id}가 낫다. - 계층 표현 :
/users/{userId}/orders/{orderId}처럼 소유 관계를 표현한다. - 행위가 필요한 경우 : REST 자원으로 표현하기 어렵다면
/orders/{id}/cancel처럼 명령 리소스를 둘 수 있다. - 버전 관리 : 큰 호환성 변경은
/v1/users처럼 URL에 넣거나, 헤더 기반 버전 전략을 사용한다.
- 명사 사용 :
- API 응답 설계
- 성공 상태 코드 : 조회는
200 OK, 생성은201 Created, 본문 없는 성공은204 No Content를 사용한다. - 에러 상태 코드 : 요청 형식 오류는
400, 인증 필요는401, 권한 없음은403, 리소스 없음은404, 충돌은409를 사용한다. - 에러 바디 : 클라이언트가 처리할 수 있도록
code,message,traceId,details같은 필드를 일관되게 제공한다.
- 성공 상태 코드 : 조회는
-
동시 수정과 조건부 요청 : 같은 자원을 여러 사용자가 수정할 수 있으면 마지막 요청이 이전 변경을 덮어쓰는 Lost Update가 생길 수 있다.
응답에 버전 또는ETag를 제공하고 클라이언트가If-Match로 기대 버전을 보내게 하면, 서버는 값이 달라진 경우412 Precondition Failed또는 업무 의미에 맞는409 Conflict로 거절할 수 있다. DB의 낙관적 락 version 컬럼과 연결하면 API부터 저장소까지 같은 동시성 규칙을 유지할 수 있다. -
재시도 가능한 명령 API와 멱등성 키(Idempotency Key) : 네트워크 timeout은 서버 처리 실패를 의미하지 않는다. 중복 실행이 위험한
POST명령에는 멱등성 키를 받아 요청 상태와 결과를 저장하고, 같은 키로 재시도되면 동일 결과를 반환한다. 키가 같지만 본문이 다르면 오류로 처리하며, 저장 기간과 정리 정책을 정한다. HTTP 메서드의 멱등성만으로 업무 동작의 중복 실행이 방지되는 것은 아니다. - REST vs RPC/gRPC 선택 기준
- REST : 자원 중심 API, 외부 공개 API, 브라우저/HTTP 생태계와의 호환성이 중요한 경우에 적합하다.
- RPC/gRPC : 행위 중심 호출, 내부 서비스 간 통신, 강한 타입 계약, 낮은 지연과 높은 처리량이 중요한 경우에 적합하다.
- 실무 판단 : 외부 클라이언트가 다양하면 REST가 단순하고, 내부 MSA에서 명확한 계약과 성능이 중요하면 gRPC를 검토한다.
- 멱등성(Idempotent)과 안전성(Safe) : HTTP 메서드의 성질
- 안전한(Safe) 메서드 : 호출해도 서버의 자원 상태를 변경하지 않는 메서드. (조회 전용)
- 멱등한(Idempotent) 메서드 : 같은 요청을 여러 번 보내도 결과(서버 상태)가 한 번 보낸 것과 동일한 메서드. 네트워크 재시도 시 안전하다. (단,
응답 코드까지 같다는 의미는 아님) -
메서드별 멱등성/안전성
메서드 안전(Safe) 멱등(Idempotent) 비고 GET O O 조회만, 상태 변경 없음 HEAD O O 본문 없는 GET POST X X 호출마다 새 자원 생성 PUT X O 같은 값으로 전체 교체 → 결과 동일 DELETE X O 여러 번 삭제해도 최종 상태 동일(이미 삭제됨) PATCH X X (구현에 따라 다름) 부분 수정, 보장되지 않음
- HTTP 상태 코드 : 응답의 결과를 나타내는 코드
- 2xx (성공) : 요청 성공. (200 OK, 201 Created — 생성됨, 204 No Content — 성공했으나 본문 없음)
- 3xx (리다이렉션) : 추가 동작 필요. (301 Moved Permanently — 영구 이동, 302 Found — 임시 이동,
304 Not Modified — 캐시 사용) - 4xx (클라이언트 오류) : 요청 자체의 문제. (400 Bad Request, 401 Unauthorized — 인증 필요,
403 Forbidden — 권한 없음, 404 Not Found, 409 Conflict) - 5xx (서버 오류) : 서버 측 처리 실패. (500 Internal Server Error, 502 Bad Gateway,
503 Service Unavailable)
- REST 성숙도 모델(Richardson Maturity Model) : REST 스타일을 어느 정도 따르는지 단계로 나눈 모델
- Level 0 : HTTP를 단순 전송 통로로만 사용 (단일 URI, POST 위주)
- Level 1 : 자원(Resource)별로 URI를 분리
- Level 2 : HTTP 메서드(GET/POST/PUT/DELETE)와 상태 코드를 규약대로 사용 (대부분의 현실 REST API가 이 단계)
- Level 3 : HATEOAS까지 적용해 응답에 다음 행동 링크를 포함
테스트 전략
-
단위 테스트(Unit Test) : 하나의 클래스나 함수 단위로 빠르게 검증한다. 외부 의존성은 mock/fake로 대체하고,
비즈니스 규칙과 예외 케이스를 촘촘히 확인하는 데 적합하다. -
통합 테스트(Integration Test) : Spring Context, DB, 외부 인프라 연동까지 포함해 실제 동작을 검증한다. Repository,
Transaction, Serialization, Security 설정처럼 단위 테스트로 놓치기 쉬운 부분을 확인한다. -
테스트 대역(Test Double) : Mock은 호출 여부와 상호작용 검증에, Fake는 간단한 동작 구현체로 상태 기반 검증에 적합하다.
과도한 mock은 리팩터링에 취약하므로 핵심 도메인 로직은 가능하면 상태 기반 테스트로 검증하는 편이 좋다. -
Testcontainers : 실제 DB, Redis, Kafka 등을 컨테이너로 띄워 테스트하는 방식. 운영과 유사한 환경에서 검증할 수 있어 신뢰도가 높지만,
실행 시간이 길어질 수 있으므로 핵심 통합 테스트에 선별적으로 사용한다.
객체지향과 디자인 패턴
- SOLID(객체지향 5대원칙)
- SRP(단일책임원칙) 모든 클래스는 하나의 책임을 갖는다. 클래스는 그 책임을 완전히 캡슐화해야 한다.
- OCP(개방-폐쇄 원칙) 확장에는 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.
- LSP(리스코프 치환 원칙) 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.
- ISP(인터페이스 분리 원칙) 큰 덩어리의 인터페이스를 구체적으로 작은 단위로 분리시켜, 꼭 필요한 메서드만 사용하게 한다.
- DIP(의존관계 역전 원칙) 상위 모듈은 하위 모듈에 의존하지 않고, 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
- 디자인 패턴
- 생성 패턴(Creational Pattern)
- Singleton(싱글톤 패턴) : 하나의 클래스 인스턴스를 전역에서 접근 가능하게 하면서 해당 인스턴스가 한 번만 생성되도록 보장하는 패턴
- Factory Method(팩토리 메서드 패턴) : 객체를 생성하기 위한 인터페이스를 정의하고, 서브클래스에서 어떤 클래스의 인스턴스를 생성할지 결정하는 패턴
- Abstract Factory(추상 팩토리 패턴) : 관련된 객체들의 집합을 생성하는 인터페이스를 제공하며,
구체적인 팩토리 클래스를 통해 객체 생성을 추상화하는 패턴 - Builder(빌더 패턴) : 복잡한 객체의 생성 과정을 단순화하고, 객체를 단계적으로 생성하며 구성하는 패턴
- Prototype(프로토타입 패턴) : 객체를 복제하여 새로운 객체를 생성하는 패턴으로, 기존 객체를 템플릿으로 사용하는 패턴
- 구조 패턴(Structural Pattern)
- Adapter(어댑터 패턴) : 인터페이스 호환성을 제공하지 않는 클래스를 사용하기 위해 래퍼(Wrapper)를 제공하는 패턴
- Bridge(브릿지 패턴) : 추상화와 구현을 분리하여 두 가지를 독립적으로 확장할 수 있는 패턴
- Composite(컴포지트 패턴) : 개별 객체와 복합 객체를 동일하게 다루어, 트리 구조의 객체를 구성하는 패턴
- Decorator(데코레이터 패턴) : 객체에 동적으로 새로운 기능을 추가하여 객체를 확장할 수 있는 패턴
- Facade(퍼사드 패턴) : 서브시스템을 더 쉽게 사용할 수 있도록 단순한 인터페이스를 제공하는 패턴
- Flyweight(플라이웨이트 패턴) : 공유 가능한 객체를 통해 메모리 사용을 최적화하는 패턴
- Proxy(프록시 패턴) : 다른 객체에 대한 대리자(Proxy)를 제공하여 접근 제어, 지연 로딩 등을 구현하는 패턴
- 행위 패턴(Behavioral Pattern)
- Observer(옵저버 패턴) : 객체 간의 일대다 종속 관계를 정의하여 한 객체의 상태 변경이 다른 객체들에게 알려지도록 한다.
- Strategy(전략 패턴) : 알고리즘을 정의하고, 실행 중에 선택할 수 있게 한다.
- Command(커맨드 패턴) : 요청을 객체로 캡슐화하여 요청을 매개변수화 하고, 요청을 큐에 저장하거나 로깅하고 실행을 지연시킨다.
- State(상태 패턴) : 객체의 상태를 캡슐화하고, 상태 전환을 관리한다.
- Chain of Responsibility(책임 연쇄 패턴) : 요청을 보내는 객체와 이를 처리하는 객체를 분리하여, 다양한 처리자 중 하나가 요청을 처리한다.
- Visitor(방문자 패턴) : 객체 구조를 순회하면서 다양한 연산을 수행할 수 있게 한다.
- Interpreter(인터프리터 패턴) : 언어나 문법에 대한 해석기를 제공하여, 주어진 언어로 표현된 문제를 해결하는 패턴
- Memento(메멘토 패턴) : 객체의 내부 상태를 저장하고 복원할 수 있는 기능을 제공하는 패턴
- Mediator(중재자 패턴) : 객체 간의 상호 작용을 캡슐화하여, 객체 간의 직접적인 통신을 방지하는 패턴
- Template Method(템플릿 메서드 패턴) : 알고리즘의 구조를 정의하면서 하위 클래스에서 각 단계의 구현을 제공하는 디자인 패턴
- Iterator(이터레이터 패턴) : 컬렉션 내의 요소들에 접근하는 방법을 표준화하여 컬렉션의 내부 구조에 독립적으로 접근할 수 있는 패턴
- 생성 패턴(Creational Pattern)
용어정리 – 운영체제
-
프로세스와 스레드 : 프로세스는 실행 중인 프로그램을 의미하고, 스레드는 프로세스 안에서 실행 흐름을 나눈 단위다.
한 프로세스 안에는 여러 개의 스레드가 생성될 수 있다. 프로세스는 운영체제로부터 독립적인 자원을 할당받지만, 스레드는 같은 프로세스의 코드/데이터/힙 영역을 공유한다. 그래서 스레드 간 통신은 효율적이지만, 공유 자원 접근에 따른 동시성 문제를 고려해야 한다. - 세마포어와 뮤텍스 : 공유 자원에 여러 프로세스·스레드가 동시에 접근할 때 동기화가 필요하다. 뮤텍스(Mutex)와 세마포어(Semaphore)는 대표적인 동기화 도구다.
- 뮤텍스(Mutex) : 공유 자원 또는 임계 영역에 하나의 Process·Thread만 접근하게 한다.
임계구역(Critical Section)을 가진 스레드들의 실행시간(Running Time)이 서로 겹치지 않고 각각 단독으로 실행(상호배제_Mutual Exclusion)되도록 하는 기술 - 세마포어(Semaphore) : permit 개수만큼 접근을 허용하는 카운터 기반 동기화 도구. permit이 1개인 binary semaphore는 뮤텍스처럼 사용할 수 있고, 여러 개면 제한된 자원을 동시에 사용할 수 있다.
- 뮤텍스(Mutex) : 공유 자원 또는 임계 영역에 하나의 Process·Thread만 접근하게 한다.
- 교착상태(Deadlock) : 둘 이상의 프로세스들이 자원을 점유한 상태에서 서로 다른 프로세스가 점유하고 있는 자원을 요구하며 무한정 기다리는 상황
- 교착상태(Deadlock) 발생 조건 : 아래 4가지가 모두 만족될 때 발생한다.
- 상호 배제(Mutual Exclusion) : 한 번에 한 프로세스만 공유 자원에 접근 가능하며, 접근 권한이 제한적일 경우.
- 점유 대기 (Hold & Wait) : 공유 자원에 대한 접근 권한을 가진 채로 다른 자원에 대한 접근 권한을 요구.
- 비선점 (Nonpreemptive) : 다른 프로세스의 자원을 뺏을 수 없음.
- 순환 대기 (Circular wait) : 두 개 이상의 프로세스가 자원 접근을 기다릴 때, 관계가 순환적 구조.
- 교착상태 해결 방법 : 크게 4가지로 나뉜다.
- 예방(Prevention) : 위 4가지 발생 조건 중 하나 이상을 원천적으로 차단. 자원 효율이 떨어진다.
- 회피(Avoidance) : 교착이 발생하지 않는 안전 상태(Safe State)를 유지하며 자원을 할당.
대표적으로 은행원 알고리즘(Banker’s Algorithm)이 있다. (자원 요청 시 할당 후에도 안전 상태가 유지되는지 미리 검사) - 탐지(Detection) : 교착을 허용하되 자원 할당 그래프 등으로 발생 여부를 주기적으로 검사.
- 회복(Recovery) : 교착이 탐지되면 프로세스 강제 종료나 자원 선점(Preemption)으로 해소.
- 교착상태(Deadlock) 발생 조건 : 아래 4가지가 모두 만족될 때 발생한다.
- 컨텍스트 스위칭(Context Switching) : CPU가 현재 실행 중인 프로세스/스레드를 잠시 멈추고 다른 것을 실행하기 위해 상태를 교체하는 작업.
- 과정 : 실행 중이던 작업의 상태(레지스터, 프로그램 카운터 등)를 PCB(Process Control Block)에 저장하고,
다음에 실행할 작업의 상태를 PCB에서 복원한다. - 비용 : 상태 저장/복원 자체의 오버헤드에 더해, 프로세스 전환 시 주소 공간 변경으로 캐시·TLB 미스가 늘 수 있다. 최신 CPU의 ASID/PCID 같은 기능은 TLB flush 비용을 줄일 수 있다. 같은 프로세스 내 스레드 전환은 메모리 공간(주소 공간)을 공유하므로 상대적으로 비용이 적다.
- 과정 : 실행 중이던 작업의 상태(레지스터, 프로그램 카운터 등)를 PCB(Process Control Block)에 저장하고,
- 선점 스케줄링 : 하나의 프로세스가 CPU를 차지하고 있을 때, 우선순위가 높은 다른 프로세스가 현재 프로세스를 중단시키고 CPU를 점유하는 스케줄링 방식
- 라운드로빈(RR): 각 프로세스는 같은 크기의 CPU를 할당 받고 선입선출에 의해 실행된다. 할당시간이 너무 크면 선입선출처럼 동작하고 너무 짧으면 오버헤드가 발생
- SRT : 짧은 시간동안 순서대로 프로세스를 수행한다. 남은 처리시간이 더 짧은 프로세스가 들어오면 해당 프로세스가 바로 선점된다.
- 다단계 큐: Ready큐를 여러 개 사용하는 기법, 각 큐는 자신의 스케줄링 알고리즘을 수행하며 큐와 큐 사이에도 우선순위가 있다.
- 비선점 스케줄링 : 프로세스가 CPU를 할당 받으면 해당 프로세스가 완료될 때까지 CPU를 사용한다. 프로세스 응답 시간 예측이 용이하며, 일괄 처리 방식에 적합
- 우선순위 : 각 프로세스 별로 우선순위가 주어지고, 우선순위에 따라 CPU를 할당
- FIFO : 프로세스들은 Ready 큐에 도착한 순서대로 CPU를 할당 받는다. 작업 완료 예측이 용이.
- SJF : 큐 안에 있는 프로세스 중 수행 시간이 짧은 것을 먼저 수행, 평균 대기시간을 감소
- HRN : 긴 작업 시간과 짧은 작업시간의 불평등을 어느정도 보장, 수행시간의 길이와 대기시간을 모두 고려하여 우선순위를 정함
- 가상 메모리(Virtual Memory) : 실제 물리 메모리(RAM)보다 큰 메모리를 사용할 수 있도록, 각 프로세스에 독립적인 논리 주소 공간을 제공하는 기법.
당장 필요한 부분만 물리 메모리에 올리고 나머지는 디스크(Swap 영역)에 두어, 메모리를 효율적으로 활용하고 프로세스 간 메모리를 격리한다.- 페이징(Paging) : 논리 메모리를 고정 크기의 페이지(Page), 물리 메모리를 같은 크기의 프레임(Frame)으로 나누어 매핑하는 방식.
외부 단편화가 없으나 내부 단편화가 발생할 수 있다. (주소 변환은 페이지 테이블, 가속은 TLB가 담당) - 세그멘테이션(Segmentation) : 메모리를 코드/데이터/스택 등 논리적 의미 단위(가변 크기 세그먼트)로 나누는 방식.
논리적 분할에 유리하나 외부 단편화가 발생한다. - 페이지 폴트(Page Fault) : 프로세스가 접근한 가상 페이지가 현재 유효하게 매핑되어 있지 않을 때 발생하는 예외다. 운영체제는 원인에 따라 파일이나 Swap에서 페이지를 읽거나 새 페이지를 할당한 뒤 실행을 재개할 수 있고, 잘못된 주소라면 프로세스에 오류를 전달한다. 디스크 I/O를 동반하는 페이지 폴트가 지나치게 잦아 실행보다 교체에 시간을 쓰는 상황을 스래싱(Thrashing)이라 한다.
- 페이징(Paging) : 논리 메모리를 고정 크기의 페이지(Page), 물리 메모리를 같은 크기의 프레임(Frame)으로 나누어 매핑하는 방식.
- 동기/비동기, 블로킹/논블로킹 : 두 기준은 다른 관점이다. 동기/비동기는 작업 완료(결과)를 누가 확인/처리하는가(관심사·순서),
블로킹/논블로킹은 호출한 쪽이 제어권을 돌려받는가(대기 여부) 를 따진다. 네 가지 조합이 가능하다.- 동기 + 블로킹 : 호출 후 결과가 나올 때까지 대기하고, 결과를 직접 받아 처리한다. (일반적인 함수 호출,
Future.get()) - 동기 + 논블로킹 : 호출은 바로 반환되지만(제어권 회수), 호출한 쪽이 완료 여부를 계속 확인(polling)하며 결과를 직접 챙긴다.
- 비동기 + 블로킹 : 작업 처리는 다른 곳에 맡기지만 결과를 기다리며 대기. (잘 쓰이지 않는 비효율적 조합)
- 비동기 + 논블로킹 : 호출은 바로 반환되고, 작업이 끝나면 콜백/이벤트로 결과를 통지받는다. I/O 대기가 많은 작업에 유리하지만, 작업 특성과 코드 복잡도를 함께 고려해야 한다. (CompletableFuture 콜백, Node.js, NIO 등)
- 동기 + 블로킹 : 호출 후 결과가 나올 때까지 대기하고, 결과를 직접 받아 처리한다. (일반적인 함수 호출,
- 스레드 안전(Thread-Safety) : 여러 스레드가 동시에 접근할 수 있는 자원이나 코드 블록이 안전하게 동작할 수 있도록 하는 개념입니다.
스레드 안전성을 확보하지 않으면, 데이터의 일관성이 깨지거나 예상치 못한 결과가 발생할 수 있다.- Critical Section (임계 구역) : 여러 스레드가 동시에 접근하면 문제가 발생할 수 있는 코드 블록이나 자원, 이 부분에서는 스레드 간의 동기화가 필요
- Race Condition (경쟁 상태) : 두 개 이상의 스레드가 동시에 자원에 접근하려고 할 때, 결과가 스레드의 실행 순서에 따라 달라지는 상태,
이로 인해 예기치 않은 동작이 발생할 수 있다. - Mutual Exclusion (상호 배제) : 임계 구역에 동시에 여러 스레드가 접근하지 못하도록 하는 메커니즘, 이를 위해 주로 락(Lock)을 사용
- Thread-Safety를 위한 주요 기법
- Locks (락) : 상호 배제를 구현하기 위한 가장 일반적인 방법. 자원에 접근할 때 락을 걸고, 작업이 끝나면 락을 해제(Mutex, Spinlock)
- Atomic Operations (원자적 연산) : 중간 상태가 관찰되지 않는 하나의 연산으로 상태를 변경한다. CPU 수준에서 지원되는 경우가 많다.
(AtomicInteger in Java) - Thread-Local Storage : 스레드마다 별도의 저장 공간을 제공하여, 스레드 간의 자원 공유를 피함.(ThreadLocal in Java)
- Immutable Objects (불변 객체) : 객체의 상태를 변경할 수 없도록 설계하여,
여러 스레드가 동시에 읽기만 할 수 있게 하고 상태 변경이 필요한 경우 새로운 객체를 생성한다. - Concurrent Collections : 동시성 문제를 해결하기 위해 설계된 컬렉션으로 내부적으로 필요한 동기화 메커니즘을 포함하고 있다.
(ConcurrentHashMap in Java)
- 사용자 모드 vs 커널 모드, 시스템 콜
- 사용자 모드(User Mode) : 일반 응용 프로그램이 실행되는 모드. 하드웨어나 핵심 자원에 직접 접근할 수 없도록 권한이 제한된다.
- 커널 모드(Kernel Mode) : 운영체제(커널)가 실행되는 모드. 모든 자원에 접근할 수 있는 특권 모드.
- 시스템 콜(System Call) : 사용자 프로그램이 파일 입출력, 네트워크, 프로세스 생성 등 커널의 기능이 필요할 때 OS에 요청하는 인터페이스.
시스템 콜이 호출되면 사용자 모드 → 커널 모드로 전환(Mode Switch)되며, 이 전환 비용 때문에 잦은 시스템 콜은 성능에 영향을 준다.
- 인터럽트(Interrupt) : CPU가 현재 작업을 잠시 중단하고 특정 이벤트를 먼저 처리하도록 알리는 신호. 처리 후에는 원래 작업으로 복귀한다.
- 하드웨어 인터럽트 : 입출력 장치, 타이머 등 외부 하드웨어가 발생시키는 비동기 신호 (키보드 입력, 디스크 완료 등)
- 소프트웨어 인터럽트(트랩, Trap) : 프로그램 실행 중 내부에서 발생 (시스템 콜, 0으로 나누기 같은 예외 등)
- ISR(Interrupt Service Routine) : 인터럽트가 발생했을 때 실행되는 처리 루틴(핸들러)
용어정리 – 컨테이너 및 오케스트레이션
Docker
-
Docker : 애플리케이션과 실행 환경을 컨테이너로 패키징해 어디서나 동일하게 실행하도록 돕는 플랫폼이다.
- 컨테이너 vs 가상머신(VM) : VM은 Hypervisor 위에 Guest OS 전체를 띄우는 반면,
컨테이너는 호스트 OS의 커널을 공유하고 프로세스 수준으로 격리한다. 따라서 컨테이너는 OS를 포함하지 않아 가볍고, 부팅이 빠르며, 자원 효율이 높다.- 리눅스 커널의 namespace(자원 격리)와 cgroup(자원 제한) 기술을 기반으로 격리를 구현한다.
- Docker 핵심 개념
- Image(이미지) : 컨테이너 실행에 필요한 파일과 설정을 담은 읽기 전용 템플릿. 변경 불가능(Immutable)하다.
- Container(컨테이너) : 이미지를 실행한 인스턴스. 이미지 위에 쓰기 가능한 레이어가 올라간 상태.
- Dockerfile : 이미지를 만드는 빌드 절차를 코드로 정의한 파일 (FROM, RUN, COPY, CMD, ENTRYPOINT 등)
- Layer(레이어) : 이미지는 여러 읽기 전용 레이어로 구성되며, 변경된 레이어만 다시 빌드/전송하므로 캐싱과 재사용에 효율적이다.
- Registry(레지스트리) : 이미지를 저장/공유하는 저장소 (Docker Hub, ECR, Harbor)
- Volume(볼륨) : 컨테이너의 쓰기 가능 레이어는 컨테이너 삭제 시 함께 사라진다. 데이터를 컨테이너 생명주기와 분리해 보관하기 위해 호스트나 별도 저장소를 마운트하는 메커니즘이다.
- 이미지 최적화
- Multi-stage Build : 빌드 단계와 실행 단계를 분리해, 최종 이미지에는 실행에 필요한 산출물만 포함시켜 이미지 크기를 줄이는 기법
- 경량 베이스 이미지 : alpine, distroless 등 작은 베이스 이미지를 사용해 용량과 공격 표면을 줄임
- 레이어 캐시 활용 : 자주 바뀌지 않는 의존성 설치를 앞 레이어에 두어 캐시 적중률을 높임 (예:
COPY pom.xml→ 의존성 다운로드 →COPY src)
- Docker Compose : 여러 컨테이너로 구성된 애플리케이션을 하나의 YAML 파일(
docker-compose.yml)로 정의하고 한 번에 실행/관리하는 도구. 주로 로컬 개발 환경 구성에 사용된다.
Kubernetes
-
Kubernetes (K8s) : 컨테이너 오케스트레이션 플랫폼. 원하는 상태를 선언하면 컨트롤러가 현재 상태를 조정해 배포, 스케일링, 복구를 자동화한다.
-
컨테이너 오케스트레이션(Orchestration) : 다수의 컨테이너를 여러 서버에 걸쳐 배포/확장/관리/복구하는 것을 자동화하는 것.
컨테이너 수가 많아지면 수동 관리가 불가능하기 때문에 필요하다. 대표 도구가 Kubernetes(K8s). - K8s 핵심 오브젝트
- Pod(파드) : 배포의 최소 단위. 하나 이상의 컨테이너를 묶은 그룹으로, 같은 Pod 내 컨테이너는 네트워크(IP)와 볼륨을 공유한다.
Pod는 언제든 죽고 재생성될 수 있어 IP가 고정되지 않는다(그래서 Service가 필요). - ReplicaSet : 지정한 수의 Pod 복제본이 항상 유지되도록 관리
- Deployment : ReplicaSet을 관리하며 롤링 업데이트/롤백 등 배포 전략을 제공하는 상위 오브젝트
- Service : 동적으로 변하는 Pod들에 대한 고정된 접근 지점(가상 IP)과 로드밸런싱을 제공
- ClusterIP : 클러스터 내부 통신용 (기본값)
- NodePort : 각 노드의 특정 포트로 외부 노출
- LoadBalancer : 클라우드 로드밸런서와 연동해 외부 노출
- Ingress : L7(HTTP/HTTPS) 레벨에서 도메인/경로 기반 라우팅을 제공하는 외부 진입점
- ConfigMap / Secret : 설정값(ConfigMap)과 민감 정보(Secret)를 컨테이너 이미지와 분리해 주입. Secret은 기본적으로 Base64 인코딩이므로, etcd 암호화·RBAC·외부 Secret Manager를 함께 검토한다.
- Namespace : 클러스터 내 자원을 논리적으로 분리하는 단위
- Volume / PV / PVC : 영속 스토리지. PV(PersistentVolume)는 실제 저장소,
PVC(PersistentVolumeClaim)는 사용자가 요청하는 스토리지 요청서
- Pod(파드) : 배포의 최소 단위. 하나 이상의 컨테이너를 묶은 그룹으로, 같은 Pod 내 컨테이너는 네트워크(IP)와 볼륨을 공유한다.
- 배포 도구와 레지스트리
- Helm : 쿠버네티스용 패키지 매니저. 여러 리소스를 하나의 묶음으로 설치/업그레이드/롤백하기 쉽게 해준다.
- Helm Chart : Helm에서 사용하는 배포 템플릿 묶음. Kubernetes 리소스 매니페스트와 설정값(values)을 함께 관리한다.
- Harbor : 쿠버네티스에서 자주 함께 쓰는 사설 컨테이너 이미지 레지스트리. 이미지 보관, 접근 제어, 취약점 관리에 활용한다.
- K8s 아키텍처
- Control Plane (Master) : 클러스터를 관리/제어하는 두뇌
- API Server : 모든 요청의 단일 진입점, 클러스터와의 통신 창구
- etcd : 클러스터의 모든 상태/설정을 저장하는 분산 Key-Value 저장소
- Scheduler : 새로운 Pod를 어느 Node에 배치할지 결정
- Controller Manager : 현재 상태를 원하는 상태로 맞추는 컨트롤러들을 실행
- Worker Node : 실제 컨테이너(Pod)가 실행되는 서버
- Kubelet : 노드에서 Pod가 정상 동작하도록 관리하는 에이전트
- Kube-proxy : Pod 간/외부 네트워크 통신 및 로드밸런싱 처리
- Container Runtime : 실제 컨테이너를 실행 (containerd 등)
- Control Plane (Master) : 클러스터를 관리/제어하는 두뇌
- K8s 운영 기능
- HPA (Horizontal Pod Autoscaler) : CPU/메모리 등 메트릭에 따라 Pod 수를 자동으로 늘리고 줄임(수평 확장)
- Self-Healing : 컨테이너가 죽으면 자동 재시작, 노드 장애 시 다른 노드로 재배치
- Probe(상태 검사)
- Liveness Probe : 컨테이너가 살아있는지 검사, 실패 시 재시작
- Readiness Probe : 트래픽을 받을 준비가 됐는지 검사, 실패 시 Service에서 제외
- Startup Probe : 기동이 느린 앱의 초기 구동 완료 여부를 검사
-
Probe와 리소스 설정의 구분 : Liveness Probe는 회복 불가능하게 멈춘 컨테이너를 재시작하기 위한 것이며,
DB가 잠시 느리다는 이유만으로 실패하게 만들면 정상 Pod를 반복 재시작할 수 있다. Readiness Probe는 트래픽을 받을 준비가 됐는지 판단해 실패 시 Service 엔드포인트에서 제외한다. 시작이 오래 걸리는 애플리케이션은 Startup Probe로 초기화 동안 Liveness 검사를 유예한다.requests는 스케줄링의 최소 보장 자원,limits는 최대 사용량이며, 메모리 limit 초과는 OOMKilled로 이어질 수 있으므로 JVM heap·메타스페이스·native memory를 포함해 산정한다. - K8s 배포 전략
- Rolling Update : Pod를 하나씩 점진적으로 교체. 무중단 배포가 가능하나 신/구 버전이 잠시 공존한다. (K8s 기본값)
- Blue-Green : 신(Green)/구(Blue) 환경을 동시에 띄우고 트래픽을 한 번에 전환. 빠른 롤백이 가능하나 자원이 두 배 필요.
- Canary : 일부 트래픽만 신버전으로 보내 검증한 뒤 점진적으로 확대
용어정리 – AI와 Spring AI
AI 전반 핵심 개념
- AI(Artificial Intelligence, 인공지능) : 사람이 수행하던 인식, 예측, 추론, 의사결정 같은 작업을 컴퓨터가 수행하도록 만드는 기술의 전체 범주다.
- Machine Learning(머신러닝) : 규칙을 모두 직접 작성하지 않고 데이터에서 패턴을 학습해 예측하거나 분류하는 AI 방식이다.
- Deep Learning(딥러닝) : 여러 층의 신경망을 이용하는 머신러닝의 한 분야다. 이미지, 음성, 자연어 처리와 생성형 AI 발전의 기반이 됐다.
- Generative AI(생성형 AI) : 학습 데이터의 패턴을 바탕으로 텍스트, 이미지, 음성, 코드 같은 새로운 결과물을 생성하는 AI다.
- Foundation Model(기반 모델) : 대규모 데이터로 사전 학습해 여러 작업에 적용할 수 있는 범용 모델이다. LLM은 언어를 중심으로 학습한 기반 모델의 한 종류다.
- LLM(Large Language Model, 대규모 언어 모델) : 많은 텍스트와 코드를 학습해 주어진 문맥에서 다음 토큰의 확률을 예측하는 방식으로 문장을 생성한다. 자연스러운 답변을 만들 수 있지만 사실을 데이터베이스처럼 저장·조회하는 시스템은 아니므로, 그럴듯하지만 틀린 답인 환각(Hallucination)이 발생할 수 있다.
- Token(토큰) : 모델이 입력과 출력을 처리하는 기본 단위다. 글자나 단어와 정확히 일치하지 않으며 토큰 수는 지연 시간과 비용에 영향을 준다.
- Context Window(컨텍스트 윈도우) : 한 번의 요청에서 모델이 참고할 수 있는 입력과 출력 토큰의 최대 범위다. 범위가 크더라도 중요한 정보가 항상 동일하게 활용되는 것은 아니다.
- Inference(추론) : 학습이 끝난 모델에 입력을 보내 결과를 생성하는 과정이다. 모델 크기, 입력·출력 토큰 수, 추론 강도 등에 따라 속도와 비용이 달라진다.
- Temperature : 다음 토큰 선택의 무작위성을 조절하는 값이다. 낮으면 결과가 비교적 일관되고, 높으면 다양성이 커지지만 정확성이 반드시 높아지는 것은 아니다.
LLM 애플리케이션 구성
-
Prompt Engineering(프롬프트 엔지니어링) : 모델이 수행할 역할, 입력, 제약 조건, 출력 형식과 예시를 명확하게 작성하는 작업이다. 프롬프트만으로 신뢰성을 보장할 수는 없으므로 구조화된 출력 검증, 도구 권한, 평가와 함께 설계한다.
-
Context Engineering(컨텍스트 엔지니어링) : 모델이 작업하는 순간에 필요한 지시, 대화 기록, 검색 문서, 도구 설명, 사용자 상태를 선택하고 배치하는 작업이다. 모든 정보를 무조건 넣으면 토큰 비용과 잡음이 커지므로 관련성, 최신성, 신뢰도를 기준으로 컨텍스트를 구성한다.
-
RAG (Retrieval-Augmented Generation, 검색 증강 생성) : LLM이 학습하지 않은 최신·사내 데이터를 외부 저장소에서 검색해 프롬프트에 함께 제공하는 기법이다. 문서 수집·분할 → 임베딩·저장 → 질문과 관련된 문서 검색 → 검색 결과를 컨텍스트로 전달하는 흐름이다. 근거를 제공해 환각을 줄일 수 있지만 검색 실패, 오래된 문서, 부적절한 분할 때문에 틀린 답을 만들 수도 있다.
-
Embedding(임베딩) : 텍스트, 이미지 등의 의미적 특징을 벡터로 변환한 값이다. 벡터 간 거리를 이용해 유사한 문서를 검색할 수 있지만, 유사도가 사실의 정확성이나 업무상 정답을 보장하지는 않는다.
-
Structured Output(구조화된 출력) : 모델 응답을 정해진 JSON Schema나 애플리케이션 DTO 형태로 받는 방식이다. 파싱 안정성을 높이지만 모델 출력은 외부 입력이므로 스키마와 업무 규칙을 서버에서 다시 검증해야 한다.
AI Agent와 Harness Engineering
-
AI Agent : 목표를 받아 모델이 계획을 세우고, 도구를 선택·실행하며, 실행 결과를 다시 관찰해 다음 행동을 결정하는 시스템이다. 일반적인 챗봇의 한 번짜리 응답과 달리
모델 판단 → 도구 실행 → 결과 관찰루프를 여러 번 수행할 수 있다. - Agent Orchestration(에이전트 오케스트레이션) : 하나 이상의 에이전트와 도구가 협력해 작업을 완료하도록 실행 순서와 상태를 조정하는 제어 방식이다. 상위 오케스트레이터가 요청을 분석해 하위 에이전트에 작업을 배분하고, 의존관계와 병렬 실행을 관리하며, 결과를 검증·통합해 다음 단계를 결정한다.
- Routing(라우팅) : 요청의 종류나 난이도에 따라 적절한 모델, 에이전트 또는 도구를 선택한다.
- Task Decomposition(작업 분해) : 큰 목표를 검색, 구현, 검증처럼 실행 가능한 하위 작업으로 나누고 의존관계를 정한다.
- Coordination(조정) : 독립 작업은 병렬로 실행하고, 선행 결과가 필요한 작업은 순차로 실행한다. 공유 상태와 중복 작업도 관리한다.
- Aggregation(결과 통합) : 여러 에이전트의 결과가 서로 충돌하거나 품질이 다를 수 있으므로 검증, 우선순위 결정, 병합 과정을 거쳐 최종 결과를 만든다.
- Failure Handling(실패 처리) : timeout, 재시도, 대체 에이전트, 부분 실패 허용, 전체 중단 조건을 명시한다.
-
오케스트레이션 선택 기준 : 역할이 분명하고 병렬화 효과가 있거나 서로 다른 권한·도구가 필요한 작업에는 여러 에이전트가 유용할 수 있다. 하지만 에이전트 수가 늘면 호출 비용, 지연 시간, 상태 동기화, 결과 충돌과 장애 지점도 증가한다. 하나의 에이전트와 명확한 도구만으로 해결할 수 있는 작업이라면 단일 에이전트 구성이 더 단순하고 예측 가능하다.
-
Function Calling / Tool Calling : 모델이 직접 실행할 수 없는 DB 조회, API 호출, 파일 수정 같은 작업을 애플리케이션의 도구에 요청하는 방식이다. 모델은 도구 이름과 인자를 제안하고 실제 권한 확인과 실행은 애플리케이션이 담당해야 한다. 입력 검증, timeout, 재시도, 멱등성, 최소 권한, 실행 횟수 제한을 함께 적용한다.
-
Agent Harness(에이전트 하네스) : 모델을 둘러싸고 작업을 실제로 수행하게 만드는 실행 시스템과 제어 계층이다. 시스템 지시, 도구, 권한, 컨텍스트 관리, 상태 저장, 작업 반복 한도, 오류 복구, 로그와 평가 체계를 포함한다. 모델이 판단을 담당한다면 하네스는 모델이 어떤 정보와 도구를 사용하고, 어디까지 실행하며, 결과를 어떻게 검증할지를 통제한다. 같은 모델도 하네스 설계에 따라 작업 성공률과 안전성이 크게 달라질 수 있다.
-
Agent Orchestration과 Agent Harness의 관계 : 오케스트레이션은 여러 작업 주체의 실행 흐름과 협업 방식에 초점을 두고, Agent Harness는 에이전트가 사용하는 지시, 도구, 권한, 상태, 검증과 관측을 포함한 전체 실행 환경을 다룬다. 오케스트레이터는 Agent Harness를 구성하는 핵심 요소가 될 수 있지만 두 용어가 같은 뜻은 아니다.
- Harness Engineering(하네스 엔지니어링) : Agent Harness를 만들고 운영하며 지속적으로 개선하는 엔지니어링 활동이다. 에이전트에게 코드 작성을 요청하는 데서 끝나지 않고, 실패 원인을 분석해 작업 규칙, 도구, 권한, 컨텍스트와 검증 피드백을 조정한다. OpenAI는 이를 환경 구성, 의도 명세, 피드백 루프 설계 중심의 엔지니어링으로 설명한다. (OpenAI Harness Engineering)
- 명확한 작업 계약 : 목표, 완료 조건, 금지 사항, 변경 가능한 범위와 검증 방법을 저장소 안의 문서와 자동화된 규칙으로 제공한다.
- 도구와 환경의 표준화 : 빌드, 테스트, 린트, 검색, 브라우저 같은 도구를 에이전트가 반복 가능하고 예측 가능한 방식으로 실행할 수 있게 한다.
- 짧은 피드백 루프 : 작은 단위로 수정하고 테스트·정적 분석·리뷰 결과를 즉시 돌려줘 잘못된 방향이 누적되지 않게 한다.
- 상태와 컨텍스트 관리 : 긴 작업은 한 컨텍스트에서 끝나지 않을 수 있으므로 계획, 진행 상황, 결정 사항과 남은 작업을 파일이나 체크포인트로 남긴다. Anthropic도 장기 실행 에이전트에서 세션 간 진행 상황을 명확한 산출물로 인계하는 방식을 제안한다. (Anthropic Long-running Agent Harnesses)
- 안전한 자율성 : 읽기와 쓰기, 내부 작업과 외부 반영을 구분하고, 위험한 명령이나 배포·결제 같은 작업에는 승인 경계를 둔다.
- Agent Harness와 Harness Engineering의 차이 : Agent Harness는 에이전트가 동작하는 시스템 또는 결과물이고, Harness Engineering은 그 시스템을 설계·검증·개선하는 활동이다. CI/CD 파이프라인과 이를 구축·개선하는 DevOps 활동의 관계와 비슷하다.
- 예를 들어 코딩 에이전트에 저장소 읽기·수정 도구, 테스트 명령, 권한 경계, 작업 기록 파일을 제공하는 환경은 Agent Harness다.
- 테스트 실패를 분석해 검증 명령을 추가하고, 과도한 파일 수정을 막도록 권한과 작업 규칙을 개선하는 일은 Harness Engineering이다.
- Harness Engineering의 핵심 이슈 : 모델 성능만 높이는 것으로는 실제 시스템의 신뢰성을 보장하기 어렵다. 잘못된 도구 선택, 과도한 권한, 컨텍스트 오염, 무한 반복, 비용 증가, 장기 작업 중 목표 이탈을 하네스에서 통제해야 한다. 반대로 규칙이 지나치게 많거나 도구 설명을 모두 컨텍스트에 넣으면 모델의 선택 정확도와 효율이 떨어질 수 있으므로, 필요한 도구를 동적으로 노출하고 피드백을 자동화하는 균형이 필요하다.
Spring AI
-
Spring AI : 스프링 진영에서 제공하는 AI 애플리케이션 개발 프레임워크. LLM(OpenAI, Anthropic Claude, Azure,
Vertex AI 등) 연동에 공통 추상화를 제공해 구현체 교체에 필요한 코드 변경을 줄인다. 다만 모델별 기능과 설정 차이까지 완전히 제거하는 것은 아니다. (Python의 LangChain과 유사한 역할) - Spring AI 핵심 개념
- ChatClient / ChatModel : LLM과 대화(프롬프트 요청/응답)하기 위한 추상화 인터페이스. 모델 구현체만 바꿔 끼우면 된다.
- Prompt / PromptTemplate : 모델에 보내는 입력. 템플릿에 변수를 주입해 동적으로 프롬프트를 구성한다.
- Advisor : ChatClient 요청·응답 흐름에 메모리, RAG, 로깅, 도구 호출 같은 공통 처리를 체인 형태로 적용하는 인터셉터다.
- Structured Output : LLM의 응답을 자바 객체나 JSON 등 구조화된 형태로 변환하는 기능이다. (OutputConverter)
- EmbeddingModel : 입력 데이터를 임베딩 벡터로 변환하는 모델 추상화다.
- Vector Store(벡터 저장소) : 임베딩된 벡터를 저장하고 유사도 기반으로 검색하는 저장소 (Redis, PGVector, Chroma 등)
-
Spring AI Tool Calling : 자바 메서드나 함수를 Tool로 등록하면 모델이 이름과 입력 스키마를 보고 호출을 요청한다. Spring AI 애플리케이션이 실제 도구를 실행하고 결과를 모델에 돌려주는 생명주기를 관리한다. (Spring AI Tool Calling)
- MCP (Model Context Protocol) : LLM 애플리케이션이 외부 데이터/도구와 표준화된 방식으로 연동하기 위한 프로토콜.
Spring AI도 MCP 클라이언트/서버를 지원한다.
AI 품질과 운영
-
Evaluation(Eval, 평가) : 모델이나 에이전트가 실제 요구사항을 얼마나 만족하는지 반복 측정하는 과정이다. 정답이 명확한 항목은 코드·스키마·테스트로 평가하고, 의미 품질은 규칙 기반 점수, 사람 평가, 별도 모델 평가를 조합한다. 모델·프롬프트·도구·검색 설정을 변경할 때 같은 평가 세트로 회귀를 확인해야 한다.
-
Evaluation Harness(평가 하네스) : 테스트 데이터 준비, 실행 환경 구성, 모델·도구 호출, 결과 수집, 채점과 리포트를 자동화하는 인프라다. 에이전트 평가는 최종 답변뿐 아니라 도구 선택, 중간 상태, 부수 효과, 비용과 완료 시간도 함께 측정해야 한다. (Anthropic Agent Evals)
-
Guardrail(가드레일) : 입력과 출력, 도구 실행을 정책에 맞게 제한하는 검증 계층이다. 금칙어 필터만 의미하는 것이 아니라 개인정보 제거, 출력 스키마 검증, 권한 검사, 허용 도구 목록, 위험 작업 승인과 실행 한도 등을 포함한다. 가드레일도 우회나 오탐이 가능하므로 단독 보안 경계로 간주하면 안 된다.
-
Prompt Injection(프롬프트 인젝션) : 사용자 입력이나 검색 문서에 숨겨진 지시가 기존 시스템 지시를 무시하게 만들거나 도구 오용을 유도하는 공격이다. 외부 콘텐츠를 명령이 아닌 데이터로 취급하고, 모델 판단과 관계없이 애플리케이션에서 권한과 인자를 검증하며, 민감한 도구와 데이터를 최소한으로 노출한다.
-
AI Observability(관측 가능성) : 모델 호출 지연, 입력·출력 토큰, 비용, 오류율, 검색 결과, 도구 호출, 평가 점수를 추적해 품질과 장애 원인을 분석하는 체계다. 프롬프트와 도구 인자에는 개인정보나 비밀값이 포함될 수 있으므로 기본 로그에 원문을 남기지 않고 마스킹과 보존 정책을 적용한다. Spring AI도 ChatClient, Model, Advisor, Tool Calling, Vector Store에 대한 메트릭과 트레이싱을 제공한다. (Spring AI Observability)
-
비결정성과 운영 기준 : 같은 입력도 모델 버전과 샘플링 설정에 따라 결과가 달라질 수 있다. 모델 이름만 기록하지 말고 모델 버전, 프롬프트 버전, 검색·도구 설정을 함께 관리하며 timeout, 재시도, fallback, 토큰·비용 한도와 응답 캐시 적용 여부를 명시한다.
