SQL의 실행과정을 아십니까..?
SQLD를 공부할 때까진 이것만 생각했었습니다. 하지만 친절한 SQL 책을 읽으면서 DBMS에 쿼리를 날리면 어떤 과정을 통해 결과집합을 얻는지 배울 수 있었습니다.
sql 실행 과정
우선 SQL은 절차적인 언어가 아닌 Structured Query Language로 구조적 질의 언어입니다. 원하는 결과집합을 구조적으로 질의하지만 결과집합을 만들어내는 과정은 절차적으로 수행해야하고 아래와 같은 과정을 거칩니다.
- SQL에 문법적, 의미상 오류가 없는지 체크하는 파싱
- 최적화 하기 쉬운 형태로 쿼리 변환을 하고 다양한 후보군이 될만한 실행계획 생성 및 비용 계산
- 실행엔진이 실행할 수 있는 프로시저로 변환하는 과정
- 프로시저 실행 후 결과반환
SQL 파싱
원래 DBMS는 I/O작업을 많이 사용하지만 1~3번의 과정은 CPU를 많이 소비하는 연산작업입니다. 따라서 파싱된 결과를 라이브러리 캐시에 저장하고 동일한 SQL을 실행할 때 파싱한 프로시저, 예상 실행계획 등을 캐싱해서 사용합니다.
SQL을 실행할 때 라이브러리 캐시를 통해 CPU 연산을 스킵하면 소프트 파싱, 캐시에서 찾지 못해 CPU 연산을 진행하는 것을 하드 파싱이라 합니다.
캐시에 저장하고 활용할 때 SQL 자체가 Key, 프로시저, 예상 실행계획 등은 Value로 활용합니다. 동일한 결과집합을 반환하는 SQL이라도 대소문자, 공백이 조금이라도 다르면 하드 파싱이 진행됩니다.
만약 로그인을 위해 다음과 같은 SQL이 사용자 수만큼 반복해서 하드파싱이 일어나면 해당 서버는 CPU 부담이 높아집니다.
select * from user where id = '사용자마다 달라짐' and password ='1234'
바인드 변수와 Mybatis
따라서 SQL의 파라미터 값만 변화하고 하드 파싱의 결과물을 재사용할 수 있도록 하기 위해선 바인드 변수를 활용해야합니다.
아래와 같이 SQL을 활용하고 바인드 변수값만 바꿔주게 된다면 SQL의 재사용성이 높아집니다.
select * from user where id = :userId and password = :usrPassword
하지만 저는 저도 모르는 사이 바인드 변수를 활용하는 개발자였는데요... 그 이유는 자바는 PreparedStatement를 통해서 바인드 변수를 활용하는 SQL을 만들어주고, Mybatis와 같은 프레임워크 내에서 #{parameter}를 통해 쿼리를 사용하면 PreparedStatement를 활용하기 때문입니다.
하지만 바인드 변수를 활용하면 컬럼의 데이터 분포 통계정보를 활용할 수 없기 때문에 올바른 실행계획으로 유도가 안될 수 있습니다. 그럴땐 Literal SQL로 변환되는 ${paramter}를 포함하는 SQL을 활용해야합니다.
이 방식을 활용할 때는 다음과 같은 유의점이 있습니다.
사용자로부터 받은 값을 이 방법으로 변경하지 않고 구문에 전달하는 건 안전하지 않다. 이건 잠재적으로 SQL 주입 공격에 노출된다. 그러므로 사용자 입력값에 대해서는 이 방법을 사용하면 안된다. 사용자 입력값에 대해서는 언제나 자체적으로 이스케이프 처리하고 체크해야 한다.
mybatis의 동적 쿼리
하지만 mybatis의 xml 내에서 수많은 if문을 통해 동일한 SQL이여도 실제 실행되는 SQL 자체가 달라지는 경우가 많은데요. SQL의 사용빈도가 높고 동적으로 변환되는 경우도 많다면 DBMS에서 사용하는 CPU 점유율이 높아지게 됩니다. 하지만 지금 하드웨어의 성능 덕분인지 별다른 문제를 만나본적은 없는데요.
만약 위와 SQL 수행빈도가 높고 동적변환이 잦아 하드 파싱으로 인해 장애가 생긴다면 적절한 SQL의 분리를 통해 소프트 파싱이 유도되게끔 하는 방법을 쓰면 되지 않을까 싶습니다. 뇌피셜입니다..
마무리
지금까지 SQL은 결과집합이 잘 나오는 것에만 집중했는데 어떤 과정을 통해 결과를 얻게 되는지 아는 것도 중요하다는걸 느꼈습니다. Mybatis에 파라미터 맵핑할 때 #{}, ${}가 있고 왜 #{}를 주로 쓰는건지 알고 쓰면 좋을 것 같습니다.
참고
친절한 SQL 튜닝