ORM을 사용하여 데이터베이스를 다루다 보면 가장 자주 마주치는 성능 저하의 원인 중 하나가 바로 N+1 문제입니다. 이 문제는 연관 관계가 설정된 엔티티를 조회할 때 발생하는데요. 예를 들어 게시글 목록을 가져오면서 각 게시글에 달린 댓글이나 작성자 정보를 함께 불러와야 하는 상황에서, 시스템이 원래 한 번의 쿼리로 해결할 수 있는 일을 데이터 개수만큼 추가로 나누어 실행하게 되는 현상을 말합니다. 처음 개발 단계나 테스트 환경에서는 데이터 양이 적어서 문제가 드러나지 않지만 실제 운영 환경에서 사용자 수가 늘어나고 데이터가 쌓이면 이 문제는 서버의 응답 속도를 급격하게 떨어뜨리는 치명적인 요소가 됩니다.

select m from Member m join fetch m.team

이 현상이 발생하는 근본적인 이유는 JPA 같은 ORM 프레임워크가 연관 관계를 처리하는 방식 때문입니다. 기본적으로 연관된 엔티티를 지연 로딩으로 설정하더라도, 프로그래밍 코드에서 루프를 돌며 각 엔티티의 연관 데이터를 참조하게 되면 그때마다 새로운 SQL 쿼리가 생성되어 데이터베이스로 전송됩니다. 이를 해결하기 위한 가장 표준적인 방법이 Fetch Join입니다. Fetch Join은 JPQL이나 SQL 수준에서 조인을 수행할 때 연관된 엔티티를 한 번에 함께 조회하도록 강제하는 기능을 의미합니다. 이를 사용하면 데이터베이스에 요청을 보내는 횟수를 획기적으로 줄일 수 있으며, 애플리케이션이 한 번의 쿼리로 필요한 모든 데이터를 가져와 메모리에 올릴 수 있어 성능이 크게 개선됩니다.

단순히 Fetch Join만 남용하기보다는 상황에 맞는 전략을 세우는 것이 중요합니다. 예를 들어 여러 개의 컬렉션(Collection)을 동시에 Fetch Join으로 묶어버리면 카테시안 곱(Cartesian Product)이 발생하여 결과 데이터가 중복되거나 성능이 오히려 저하될 수 있기 때문입니다. 이런 경우에는 BatchSize 설정을 통해 IN 절을 활용해 쿼리 횟수를 조절하거나, 필요한 시점에만 DTO로 직접 조회하는 방식을 병행해야 합니다. 결국 실무에서는 연관 관계의 성격과 데이터의 양을 고려하여 Fetch Join을 기본으로 하되 상황에 맞는 최적화 기법을 함께 적용하며 문제를 해결해 나가는 과정이 필요합니다.

데이터베이스를 운영하다 보면 갑자기 문자가 잘리거나 검색 결과가 예상과 다르게 나오는 경우가 있어요. 대부분 테이블이나 컬럼의 문자셋 설정이 기본값인 latin1이나 MySQL 5.7 이하의 utf8 그대로일 때 발생하거든요. 이 버전의 utf8은 UTF-8 인코딩을 최대 3바이트로 제한한 것으로, BMP에 해당하는 문자만 표현할 수 있어 4바이트가 필요한 이모지나 보조평면 문자를 담지 못해요. 그래서 저장되지 않거나 오류가 뜨는 거예요. 최신 환경에서는 4바이트를 지원하는 utf8mb4를 권장하고 있어요.

ALTER DATABASE mydb CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;

ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;

SHOW FULL COLUMNS FROM users LIKE '%name%';

서버 전체 기본값을 변경하려면 my.cnf 파일의 mysqld 섹션에 character-set-server=utf8mb4와 collation-server=utf8mb4_0900_ai_ci를 추가한 뒤 재시작하면 돼요. 기존 데이터베이스는 alter database 명령으로, 테이블은 alter table 명령으로 각각 문자셋을 변경할 수 있어요. 연결 시에도 설정이 일치해야 해요. jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=UTF-8 형태로 파라미터를 추가해야 해요. characterEncoding 파라미터는 자바 표준 문자셋 이름을 기대하므로 UTF-8로 설정하는 게 맞고, utf8mb4 적용 여부는 서버 설정과 드라이버 핸드셰이크 과정에서 자동으로 결정되거든요. 정렬 규칙은 utf8mb4_general_ci보다 성능과 정확성이 개선된 utf8mb4_0900_ai_ci를 쓰는 게 좋아요. collation 변경 전 반드시 백업을 진행해야 하는 이유는 기존 데이터의 바이트 배열이 재해석되면서 순서가 달라질 수 있기 때문이에요. 현재 설정은 SHOW VARIABLES LIKE character_set% 명령으로 확인할 수 있어요. Spring Boot를 쓴다면 application.yml에 spring.datasource.hikari.connection-init-sql을 함께 등록하면 더 안정적이에요. 연결 풀에서 초기 커넥션을 생성할 때 문자셋을 명시하면 매번 설정하는 번거로움을 줄일 수 있어요. HikariCP를 쓴다면 poolName과 connectionTestQuery를 함께 확인하고, 데이터베이스 마이그레이션 툴을 사용 중이라면 초기 스크립트에 charset 설정을 포함시키는 게 좋아요. 문자셋 문제는 애플리케이션 코드까지 고치면 번거로워지니까 초기 단계에서 확실히 잡아두는 게 가장 깔끔한 해결책이네요. 설정 값만 정확히 맞춰도 한글과 이모지 저장, 정렬 검색 모두 문제없이 돌아갈 거예요.

서버를 운영하는 분들이면 누구나 한 번쯤은 겪는 현상인데요. DB 연결이 갑자기 끊기거나 타임아웃 오류가 뜨는 일이죠. 저도 초기에 설정을 대충 넘기다가 프로덕션에서 이상한 에러를 마주해서 꽤 고생했던 기억이 납니다. 사실 커넥션 풀의 기본값만 믿고 넘어가면 안 되는 이유가 있거든요. 네트워크 장비나 클라우드 환경마다 유휴 연결을 정리하는 기준이 제각각이기 때문이에요. 기본값은 모든 환경에 완벽히 맞지 않기 때문이죠.

대부분의 프레임워크에서 기본으로 들어가는 HikariCP를 예로 들면, maxLifetime와 keepaliveTime 설정이 핵심입니다. maxLifetime는 커넥션이 생성된 후 얼마나 유지될지를 결정하는 값이고요. 보통 방화벽이나 로드밸런서가 중간에 연결을 끊어버리는 경우가 많기 때문에 이 값을 실제 네트워크 타임아웃보다 30초 정도 짧게 잡아주는 게 안전합니다. 그런데 여기서 잠깐! keepaliveTime은 유휴 상태의 커넥션이 살아있다는 신호를 주기적으로 보내는 설정이에요. 이게 없으면 방화벽이 저 커넥션 죽었나 하고 중간에 차단해버리는 경우가 많더라고요. 두 값을 함께 맞춰주면 연결 끊김 현상을 크게 줄일 수 있어요.

spring:
  datasource:
    hikari:
      max-lifetime: 1800000
      keepalive-time: 30000

위 설정은 커넥션을 최대 30분까지만 유지하고, 30초마다 유휴 상태에 핑을 보내는 예시예요. 실제 프로젝트에서는 데이터베이스와 클라우드 제공자의 네트워크 정책 파일을 꼭 참고해서 값을 맞춰주시면 됩니다. 만약 새로 연결을 맺는 시간이 오래 걸린다면 connectionTimeout 값도 함께 조정해주시면 좋겠어요. 로컬에서 테스트할 때는 값이 너무 작지 않도록 주의하셔야 해요. 초보자가 보기엔 설정이 많게 느껴지지만, 실제 운영하다 보면 이 기본기만 잘 맞춰도 연결 관련 이슈의 절반은 해결될 거예요. 익숙해질 때까지는 공식 레퍼런스 한 번쯤 훑어보시는 걸 추천드려요. 설정을 조금씩 조정해 가며 모니터링하는 습관이 정말 중요하답니다. 안정적인 서비스 운영을 위해 꼭 체크해 보세요.

인덱스는 분명히 걸어뒀는데 쿼리가 여전히 느린 경우가 있거든요. 저도 처음엔 인덱스를 잘못 만든 줄 알고 몇 번이나 다시 만들어봤는데, 알고 보니 인덱스가 아니라 WHERE 절을 쓰는 방식이 문제였더라고요. EXPLAIN을 찍었을 때 type이 ALL이고 key가 NULL이면 인덱스를 아예 안 타고 풀스캔을 돌고 있는 겁니다.

가장 흔한 원인은 묵시적 형변환이에요. phone 컬럼이 VARCHAR인데 따옴표를 빼먹고 숫자로 비교하면, MySQL 공식 문서에도 문자열 컬럼을 숫자와 비교할 때는 인덱스를 써서 값을 빠르게 찾을 수 없다고 명시돼 있어요. 양쪽을 부동소수점으로 바꿔서 비교해버리거든요. 따옴표 하나 붙였을 뿐인데 몇 초 걸리던 쿼리가 밀리초로 줄어드는 걸 보면 좀 허무하기도 합니다.

두 번째는 컬럼을 함수로 감싸는 경우예요. 날짜 컬럼을 DATE 함수로 감싸서 비교하는 조건은 인덱스에 저장된 원본 값이 아니라 가공된 값을 찾는 셈이라 인덱스가 무용지물이 돼요. 이럴 땐 함수를 컬럼이 아니라 조건 쪽으로 옮겨서 범위 조건으로 풀어주는 게 정석입니다. 앞에 와일드카드가 붙는 LIKE 검색도 같은 이유로 인덱스를 못 탑니다. 복합 인덱스도 마찬가지라서 세 컬럼으로 묶어놓고 가운데 컬럼만 조건으로 주면, 왼쪽부터 순서대로 쓰이는 규칙 때문에 그냥 무시되더라고요.

말로만 하면 감이 잘 안 오니까 잘못된 쿼리와 고친 쿼리를 나란히 놓아볼게요.

-- 인덱스를 못 타는 쿼리
-- phone 은 VARCHAR 인데 숫자로 비교 (묵시적 형변환)
SELECT * FROM users WHERE phone = 01012345678;

-- 컬럼을 함수로 감쌈
SELECT * FROM orders WHERE DATE(created_at) = '2026-07-14';

-- 앞쪽 와일드카드
SELECT * FROM users WHERE name LIKE '%인드라';


-- 인덱스를 타도록 고친 쿼리
SELECT * FROM users WHERE phone = '01012345678';

SELECT * FROM orders
 WHERE created_at >= '2026-07-14 00:00:00'
   AND created_at <  '2026-07-15 00:00:00';


-- 표현식으로 꼭 찾아야 한다면 함수 인덱스 (MySQL 8.0.13+)
CREATE INDEX idx_orders_created_date
    ON orders ((DATE(created_at)));

-- 확인은 항상 EXPLAIN 으로
EXPLAIN SELECT * FROM users WHERE phone = '01012345678';

정 표현식으로 검색해야 하는 상황이면 MySQL 8.0.13부터 지원하는 함수 인덱스를 쓸 수 있어요. 표현식을 괄호로 한 번 더 감싸서 인덱스를 만드는 방식인데, 내부적으로는 숨겨진 가상 생성 컬럼으로 구현된다고 하네요. 다만 인덱스를 만들 때 쓴 표현식과 쿼리에 쓴 표현식이 똑같아야만 인덱스를 타니까, 만들어놓고 EXPLAIN으로 실제로 타는지 한 번은 꼭 확인해보시는 게 좋아요.

결국 인덱스가 안 먹는 문제는 인덱스를 더 만들어서가 아니라 쿼리를 고쳐서 풀리는 경우가 대부분이더라고요. 느린 쿼리를 만나면 인덱스부터 추가하기 전에 EXPLAIN을 먼저 찍어보는 습관을 들이면 시간을 많이 아낄 수 있습니다.

출처: MySQL 8.0 Reference Manual - Type Conversion in Expression Evaluation (https://dev.mysql.com/doc/refman/8.0/en/type-conversion.html), CREATE INDEX Statement (https://dev.mysql.com/doc/refman/8.0/en/create-index.html)

출처: https://wogus789789.tistory.com/266

SELECT
	info. TABLE_NAME,
	info. COLUMN_NAME,
	info.udt_name as type,
	case when info.character_maximum_length is null then info.numeric_precision else info.character_maximum_length end as length,
	info.column_default,
	info.is_nullable,
	comm.column_comment as comment,
	case when pri_key.column_name is null then '' else 'PK' end as PK
FROM
	information_schema. COLUMNS info
LEFT JOIN (
	SELECT
		PS.schemaname as SCHEMA_NAME,
		PS.RELNAME AS TABLE_NAME,
		PA.ATTNAME AS COLUMN_NAME,
		PD.DESCRIPTION AS COLUMN_COMMENT
	FROM
		PG_STAT_ALL_TABLES PS,
		PG_DESCRIPTION PD,
		PG_ATTRIBUTE PA
	WHERE
		PS.RELID = PD.OBJOID
	AND PD.OBJSUBID <> 0
	AND PD.OBJOID = PA.ATTRELID
	AND PD.OBJSUBID = PA.ATTNUM
	ORDER BY
		PS.RELNAME,
		PD.OBJSUBID
) comm ON comm.SCHEMA_NAME = info.table_schema
AND comm. TABLE_NAME = info. TABLE_NAME
AND comm. COLUMN_NAME = info. COLUMN_NAME
LEFT JOIN (
	SELECT
		CC.*
	FROM
		INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC,
		INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE CC
	WHERE
		TC.CONSTRAINT_TYPE = 'PRIMARY KEY'
   AND TC.TABLE_CATALOG   = CC.TABLE_CATALOG
   AND TC.TABLE_SCHEMA    = CC.TABLE_SCHEMA
   AND TC.TABLE_NAME      = CC.TABLE_NAME
   AND TC.CONSTRAINT_NAME = CC.CONSTRAINT_NAME
) pri_key ON pri_key.table_schema = info.table_schema
AND pri_key. table_name = info.TABLE_NAME
AND pri_key. column_name = info. COLUMN_NAME
WHERE
	info.table_schema = 'public'
ORDER BY
	info. TABLE_NAME,
	info.ordinal_position;

grant 명령어로 권한을 주는데, 제목과 같은 오류가 발생하면서 제대로 설정이 되지 않았다.

검색해보니 여러가지 절차를 거쳐서 수정하는 방안이 있는데, 좀 더 간단한 방법이 없나해서 찾아보니 다음과 같은 명령어를 통해 해결할 수 있었다
제목에서의 오류는 mysql.db 이다.

repair table [DB].[테이블] use_frm;

repair table mysql.db use_frm;

CONVERT_TZ(now(), @@session.time_zone, '+00:00')



1. cassandra DB란?

• 분산 대용량 저장소(NoSQL)

아파치 라이센스: Facebook에서  개발 시작, 이후 오픈 소스 화

상용 구글의 BigTable이나 아마존의 Dynamo과 목표는 비슷함

WriteRead보다 빠르고, ReadRDB보다 빠름

P2P처럼 인터넷 상의 IDC를 커버하는 망 구성 가능

서비스 중에 노드를 추가하여 용량 확장 가능

Key-Value 구조에 RDBTable과 비슷한 스키마 지원

SQL과 유사한 CQL 지원

2. 논리적 데이터 구조
• Column
- 가장 최소단위
- name, value, timestamp로 구성 (name은 column 의 name)
• Columns
- 하나 이상의 column의 집합 (RDB의 Row)
• Super Column
- Column혹은 Super Column을 value로 가질 수 있는 Column
- name, value로 구성
• Column Family
- RDB의 Table에 해당
- key와 value의 데이터들로 구성
- value는 Columns혹은 Super Column이 올 수 있음
• Keyspace
- RDB의 Database에 해당
- Column Family의 집합

3. 물리적 구조
• Node
- 하나의 cassandra db 프로세스
- 하나의 서버(H/W)에 여러개의 cassandra db 프로세스가 실행 될 수 있음(여러개의 Node가 됨)
• Rack
- IDC내의 서버(H/W)가 장착되는 장비
• DataCenter
- 인터넷에 있는 IDC
• TokenRing
- 각각의 Node를 하나의 Ring으로 구성
- Key를 해쉬하여 나온 값을 token으로 보고 해당 token이 속해있는 Node에 데이터 저장
- 데이터 복제본은 정책에 따라서 이웃Node에 저장하거나 임의의 Node에 분산 저장

4. 통신 보안
• 공통
- 기본적으로 TLS와 PKI를 지원
 Node와 Node간에는 설정에 따라서 DC - DC or Rack - Rack or all 구간에 암호화
 Client와 Node간에 암호화 설정 가능
• 데이터 암호화는 지원하지 않음

5. 계정
• cassandra db는 기본 생성 시 super user인 cassandra계정만 존재
• 계정 추가는 CQL로 하는게 편함
• CQL에서 권한설정 가능(grant)

http://www.sqlite.org/speed.html

자세한건 링크 고고싱

+ Recent posts