https://jupiny.com/2018/05/07/git-rebase-i-option/
-i : interactive mode(대화형)
pick : 커밋 그대로 사용
reword : 커밋 메시지 변경
edit : 커밋 메시지 + 작업 내용 변경
squash : 이전 커밋과 합침
fix : squash 처럼 이전 커밋과 합치는데, 커밋 메시지는 합치지 않고 이전 커밋 메시지를 그대로 사용
더 많은 것을 기억하기 위해 기록합니다
https://jupiny.com/2018/05/07/git-rebase-i-option/
-i : interactive mode(대화형)
pick : 커밋 그대로 사용
reword : 커밋 메시지 변경
edit : 커밋 메시지 + 작업 내용 변경
squash : 이전 커밋과 합침
fix : squash 처럼 이전 커밋과 합치는데, 커밋 메시지는 합치지 않고 이전 커밋 메시지를 그대로 사용
앞서 우리가 HTTP 요청을 모델에 바인딩하고 클라이언트에 보낼 HTTP 응답을 만들기 위해 뷰를 사용했던 방식과는 달리,
HTTP 요청 본문과 HTTP 응답 본문을 통째로 메세지로 다루는 방식이다.
주로 XML
이나 JSON
을 이용한 AJAX 기능이나 웹 서비스를 개발할 때 사용된다.
아래와 같이 스프링의 @RequestBody
와 @ResponseBody
를 통해 구현할 수 있다.
1 | @ResponseBody // 응답 |
위와 같은 애노테이션을 명시해두게 되면 스프링은 메세지 컨버터
라는 것을 사용하여 HTTP 요청이나 응답을 메세지로 변환하게 된다.
즉 위처럼 파라미터 부분에 @RequestBody
를 입력할 경우, 파라미터 타입에 맞는 메세지 컨버터를 선택한 뒤 HTTP 요청 본문을 통째로 메세지로 변환하여 파라미터에 바인딩하는 것이다.
메서드의 상단에 @ResponseBody
를 입력할 경우 또한 마찬가지로 리턴 타입에 맞는 메세지 컨버터를 선택한 뒤 리턴 값을 통째로 메세지로 변환한 뒤 리턴해주는 것이다.
참고로 GET 방식의 요청일 경우 HTTP 요청 본문이 없으므로
@RequestBody
를 사용할 수 없다.@RequestParam
이나@ModelAttribute
를 사용해야 한다.
이렇게 사용되는 메세지 컨버터는 AnnotationMethodHandlerAdapter
를 통해 등록할 수 있고, 이미 디폴트로 4가지의 메세지 컨버터가 등록되어 있다.
아래는 디폴트 메세지 컨버터들이다.
지원하는 오브젝트 타입은 byte[]
이고, 미디어타입은 모든 것을 다 지원한다.
즉 파라미터에 @RequestBody byte[] param
과 같이 작성하면 모든 요청을 다 byte
배열로 받을 수 있다는 말이다.
그리고 리턴타입을 byte[]
로 했을 경우 Content-Type
이 applcation/octet-stream
으로 설정되어 전달된다.
바이너리 정보를 주고받을 경우가 아니라면 그닥 유용해 보이진 않는다.
지원하는 오브젝트 타입은 String
이고, 미디어타입은 모든 것을 다 지원한다.
파라미터에 사용할 경우 HTTP 본문을 그대로 String
으로 가져올 수 있게되고,
리턴에 사용할 경우 단순 문자열을 그대로 전달해줄 수 있다.
Content-Type
은 text/plain
으로 전달된다.
지원하는 오브젝트 타입은 MultiValueMap<String, String>
이고, 미디어타입은 application/x-www-form-urlencoded
만 지원한다.
즉 정의된 폼 데이터를 주고받을 때 사용할 수 있다는 말인데,
폼 데이터는 @ModelAttribute
를 사용하는 것이 훨씬 유용하므로 이것 또한 자주 사용할 일은 없다.
지원하는 오브젝트 타입은 DomSource
, SAXSource
, StreamSource
이고, 미디어타입은 application/xml
, application/*+xml
, text/xml
세가지를 지원한다.
XML 문서를 Source 타입의 오브젝트로 변환하고 싶을 떄 사용할 수 있다.
하지만 요즘은 OXM 기술이 많이 발달되었으므로 이 또한 잘 쓰이지 않는다.
이제 아래는 디폴트가 아닌 메세지 컨버터들이다. 실제로 이 컨버터들이 더 유용하다.
여기서 필요한게 있다면 직접 AnnotationMethodHanlderAdapter
의 messageConverters
에 등록하고 사용해야 한다.
1 | <bean class="org.springframework...AnnotationMethodHandlerAdapter"> |
다른 전략과 마찬가지로 위와 같이 등록 시 디폴트 전략이 모두 무시된다는 점에 주의해야 한다.
JAXB의 @XmlRootElement
와 @XmlType
이 붙은 클래스를 이용해 XML과 오브젝트 사이의 메세지 변환을 지원한다.
지원하는 미디어 타입은 SourceHttpMessageConvreter
와 동일하다.
스프링 OXM 추상화의 Mashaller
와 Unmarshaller
를 이용해서 XML과 오브젝트 사이의 변환을 지원한다.
이 컨버터를 등록할 때는 marshaller
와 unmarshaller
를 설정해줘야 한다.
지원하는 미디어 타입은 SourceHttpMessageConvreter
와 동일하다.
Jackson
의 ObjectMapper
를 이용해서 JSON과 오브젝트 사이의 변환을 지원한다.
지원하는 미디어 타입은 application/json
이다.
변환하는 오브젝트 타입의 제한은 없지만 프로퍼티를 가진 자바빈 스타일이나 HashMap을 이용해야 정확한 변환 결과를 얻을 수 있다.
모든 일에는 계획이 필요하다. 일을 하든… 여행을 가든…
그리고 우리는 그 일을 처리하기 위한 여러가지 계획을 세우고, 그 중에서 어떤 방식이 최적이고 최소의 비용이 소모되는지를 결정하게 된다.
이는 DBMS도 마찬가지이다. 옵티마이저는 쿼리를 실행하기전 여러가지 통계정보를 참조하여 최적의 계획을 세우고, 그 계획대로 쿼리를 실행한다.
옵티마이저
SQL을 가장 빠르고 효율적으로 수행할 최적의 경로를 생성하는 DBMS 내부 핵심엔진이다.
즉 데이터베이스 서버에서 두뇌와 같은 역할을 담당한다.
옵티마이저의 최적화 방법으로는규칙 기반 최적화(RBO)
와비용 기반 최적화(CBO)
가 있는데, 현재는 거의 대부분의 DBMS에서 비용 기반 최적화를 사용하고 있다.
MySQL 또한 마찬가지이다.
통계 정보
비용 기반 최적화에서 실행계획 수립 시 가장 중요하게 사용되는 정보이다.
통계 정보가 정확하지 않으면 전혀 엉뚱한 방향으로 쿼리를 실행해 버릴 수 있기 때문이다.
이 통계정보는ANALYZE
라는 명령어를 사용해 직접 갱신할 수 있는데, MySQL의 경우 사용자가 알아채지 못하는 사이에 자동으로 계속 변경되기 때문에 직접 수동으로 갱신할 일은 별로 없다.
(하지만 레코드 건수가 작으면 통계 정보가 부정확할 때가 많음)
InnoDB
의 경우ANALYZE
를 실행하는 동안 읽기와 쓰기가 모두 불가능하므로 서비스 도중에는 실행하지 않는것이 좋다.
MySQL 서버에서 쿼리가 실행되는 과정은 크게 3가지로 나눌 수 있다.
1번 단계를 SQL 파싱
이라고 하고, MySQL 서버의 SQL 파서
라는 모듈로 처리한다.
이 단계에서 만들어진 것을 SQL 파스 트리
라고 한다.
2번 단계에서는 1번 단계에서 생성된 SQL 파스 트리
를 참조하여 옵티마이저에서 다음 내용을 처리한다.
3번 단계에서는 2번 단계에서 수립된 실행 계획대로 스토리지 엔진에 레코드를 읽어오도록 요청하고,
받은 레코드를 MySQL 엔진이 조인하거나 정렬하는 작업을 수행한다.
MySQL은 스토리지 엔진과 MySQL 엔진으로 구분된다.
스토리지 엔진은 디스크나 메모리상에서 필요한 레코드를 읽거나 저장하는 역할을 하며,
MySQL 엔진은 스토리지 엔진으로부터 받은 레코드를 가공/연산하는 작업을 수행한다.
보다시피 1,2번 단계는 거의 MySQL 엔진에서 처리하며, 3번 단계는 MySQL 엔진과 스토리지 엔진이 동시에 참여해서 처리한다.
쿼리에 EXPLAIN
이라는 명령어를 추가로 사용하면 MySQL이 수립한 실행계획을 직접 볼수있다.
아래는 실행계획의 예시이다.
표의 각 라인은 사용된 테이블의 개수(임시 테이블 포함)이고,
실행순서는 대체적으로 위에서 아래로 진행된다.
참고로 실행계획은 SELECT
문만 확인 가능하며, DML 문장의 실행계획을 확인하고 싶으면 WHERE
조건절만 같은 SELECT
문을 만들어서 대략적으로 확인해보는 수 밖에 없다.
SELECT 단위 쿼리별로 부여되는 식별자 값
이다.
하지만 만약 JOIN
을 했을 경우, 레코드는 테이블의 개수만큼 출력되지만 id는 동일하게 부여된다.
JOIN
시 먼저(윗 라인)에 표시된 테이블이 드라이빙 테이블
, 이후에 표시된 테이블이 드리븐 테이블
이 된다.
각 단위 SELECT가 어떤 타입의 쿼리인지 표시하는 칼럼이다.
UNION
이나 서브 쿼리
를 사용하지 않는 단순한 SELECT
쿼리인 경우 표시된다.
UNION
이나 서브 쿼리
가 포함된 SELECT
쿼리의 실행계획에서 가장 바깥쪽에 있는 단위쿼리
인 경우 표시된다.
UNION
이나 UNION ALL
로 결합하는 단위 SELECT
쿼리들 중 첫번째를 제외한 두번째 이후부터 표시된다.
첫번째 레코드에는 UNION
대신 DERIVED
가 표시된다.
조회된 결과를 UNION
으로 결합해 임시테이블을 만들어 사용하기 떄문이다.
UNION
/UNION ALL
을 사용하는 단위쿼리가 Outer 쿼리에 의해 영향을 받을 경우 표시된다.
1 | EXPLAIN |
예외가 조금 억지스럽긴 하다…
보다시피 UNION
에서 Outer 쿼리의 emp_no
칼럼을 이용했기 때문에 DEPENDENT UNION
이 표시되고 있다.
위와 같은 형태를
서브 쿼리
라고 하는데, 일반적으로 서브 쿼리는 Outer 쿼리보다 먼저 실행되며, 속도도 빠르게 처리된다.
하지만 위와 같이 Outer 쿼리에 의존적인 서브쿼리, 즉DEPENDENT
형태의 경우 절대 Outer 쿼리보다 먼저 실행될 수 없다.
그래서DEPENDENT
실행계획이 포함된 쿼리는 비효율적인 경우가 많다.
하나의 단위쿼리가 다른 단위쿼리를 포함했을 경우 이를 서브쿼리 라고 하는데, SUBQUERY
select_type은 FROM절 이외에서 사용되는 서브쿼리만을 의미한다.
FROM절에 사용된 서브쿼리는 select_type이 DERIVED
로 표시된다.
DEPENDENT UNION
과 같이 서브쿼리가 Outer 쿼리에 정의된 컬럼을 사용하는 경우 표시된다.
이 또한 일반 서브쿼리보다 처리속도가 느린 경우가 많다.
단위 SELECT
쿼리의 실행 결과를 메모리나 디스크의 임시 테이블을 생성하여 저장할 때 표시된다.
MySQL은 FROM
절에 사용된 서브쿼리를 제대로 최적화하지 못할 경우가 대부분이다.(인덱스가 전혀 없으므로)
그에 비해 MySQL 5.0 이후로는 조인이 상당히 최적화 된 편이므로, FROM
서브쿼리 대신 조인을 사용하는 것이 좋다.
옵티마이저는 조건이 똑같은 서브쿼리의 실행결과는 내부적인 캐시 공간에 담아둔 뒤 다시 사용하며 성능을 향상시킨다.
SUBQUERY
와 DEPENDENT SUBQUERY
가 캐시를 사용하는 방법은 다음과 같다.
SUBQUERY
: Outer 쿼리의 영향을 받지 않으므로 처음 한번만 실행해서 결과를 캐시하고, 필요할 떄 이용한다.DEPENDENT SUBQUERY
: Outer 쿼리 컬럼의 값 단위로 캐시해두고 사용한다.
UNCACHEABLE SUBQUERY
의 경우 캐시를 하지 못하는 경우 표시되는데, 이유는 다음과 같다.
- 시용자 변수가 서브쿼리에 포함된 경우
- NOT_DETERMINISTIC 속성의 스토어드 루틴이 서브쿼리에 사용된 경우
UUID()
나RAND()
같이 호출할 때 마다 달라지는 함수가 서브쿼리에 사용된 경우
위와 동일하게 UNION
결과를 캐시할 수 없을 경우 사용된다.
MySQL의 실행계획은 SELECT
쿼리 기준이 아니라 테이블 기준으로 표시된다.
alias
(별칭)를 사용했을 경우 alias
가 표시되고, 테이블을 사용하지 않았을 경우 NULL
이 표시된다.
그리고 테이블 이름이 < >
같이 둘러싸였을 경우, 임시테이블을 의미한다.
MySQL 서버가 각 테이블의 레코드를 어떤 방식으로 읽었는지를 표시해준다.
이 컬럼을 통해 인덱스를 사용했는지, 테이블을 풀 스캔했는지 등을 확인할 수 있다.
인덱스를 효율적으로 사용하는 것은 매우 중요하므로, 이 컬럼은 꼭 확인해야 할 정보이다.
아래는 MySQL에서 부여한 접근속도 순위이다. ALL
타입만 빼고 모두 인덱스를 사용하는 방식이다.
레코드가 1건만 존재하거나 1건도 존재하지 않는 테이블을 참조할 떄 표시된다.
이는 InnoDB에서는 나타나지 않고, MyISAM이나 MEMORY 테이블에서 사용되는 접근 방식이다.
테이블 레코드 건수에 관계없이 WHERE 조건절에서 프라이머리 키나 유니크 키 컬럼을 사용하며, 반드시 1건만 반환할 경우 표시된다.
1 | EXPLAIN |
다중컬럼으로 구성된 프라이머리 키나 유니크 키의 일부 컬럼만 사용할 경우 const
타입의 접근 방법을 사용할 수 없다.
여러 테이블이 조인되는 쿼리의 실행계획에서만 표시된다.
조인에서 처음 읽은 테이블의 컬럼 값을 그 다음 읽어야 할 테이블의 프라이머리 키나 유니크 키 컬럼 검색 조건에 사용하고,
그로 인해 두번쨰 테이블에서 출력되는 레코드가 반드시 1건이라는 보장이 있을 경우 표시된다.
1 | EXPLAIN |
인덱스를 Equal 조건으로 검색할 때 사용된다.
조인의 순서와 관계없고, 프라이머리 키나 유니크 키 등의 제약조건도 없다.
반환되는 레코드가 반드시 1건이라는 보장이 없으므로 const
나 eq_ref
보다는 느리나, 기본적으로 매우 빠른 조회방법 중 하나이다.
ref
와 같은데 NULL
비교가 추가된 형태이다.
1 | EXPLAIN |
실무에서 별로 사용되지 않으므로 이 정도만 기억해도 된다.
WHERE 조건절에서 사용될 수 있는 IN (subquery)
형태의 쿼리를 위한 접근 방식이다.
서브쿼리에서 중복되지 않은 유니크한 값만 반환될 때 표시된다.
IN
연산자의 특성상 괄호안에 있는 값의 목록에는 중복이 먼저 제거되어야 한다.
index_subquery
의 경우 서브쿼리가 중복된 값을 반환할 수 있지만, 인덱스를 이용해 중복을 제거할 수 있을때 표시된다.
인덱스를 하나의 값이 아니라 범위로 검색하는 경우에 표시된다.
범위 검색 연산자의 경우 <
, >
, IS NULL
, BETWEEN
, IN
, LIKE
등이 있다.
2개 이상의 인덱스를 이용해 각각의 검색 결과를 만들어 낸 후, 이를 병합하여 처리하는 방식이다.
1 | EXPLAIN |
하지만 index_merge
의 경우 이름처럼 효율적으로 작동하는 경우가 그렇게 많지는 않다.
이름만 보면 아주 좋아보이나, 실제로는 인덱스를 처음부터 끝까지 읽는 인덱스 풀 스캔을 의미한다.
풀 테이블 스캔과 읽는 레코드 수는 같으나, 인덱스가 일반적으로 데이터 파일 전체보다는 크기도 작고 정렬도 되어있으므로 풀 테이블 스캔보다는 빨리 처리된다.
이 방식은 다음의 조건을 충족할 떄 표시된다.
range
, const
, ref
와 같은 방식으로 인덱스를 이용하지 못하는 경우풀 테이블 스캔을 의미한다.
테이블을 처음부터 끝까지 다 읽는 방식으로, 가장 비효율적인 방법이다.
옵티마이저가 최적화 된 실행계획을 만들기 위해 후보로 선정했던 인덱스의 목록이다.
즉, “사용될 뻔 했던 인덱스 목록” 이므로, 아무 도움도 되지 않는다. 그냥 무시하자.
possible_keys
와 달리 최종 실행계획에서 선택된 인덱스를 의미한다.
그러므로 쿼리 튜닝 시 의도했던 인덱스가 표시되는지 이곳을 통해 확인하는 것이 중요하다.
2개 이상의 인덱스가 사용될 경우 ,
로 구분되어 표시된다.
프라이머리 키의 경우 PRIMARY KEY
라는 이름으로 표시된다.
실제 업무에서는 단일 컬럼 인덱스보다 다중 컬럼으로 만들어진 인덱스가 더 많은데, key_len
은 쿼리를 처리하기 위해 다중 컬럼으로 구성된 인덱스에서 몇 개의 컬럼까지 사용했는지 알려준다.
정확히는 몇 바이트까지 사용했는지 알려준다.
1 | EXPLAIN |
PRIMARY KEY
의 4바이트만을 이용했다고 표시되고 있다.
emp_no
은 INTEGER
타입으로써 저장공간으로 4바이트를 사용한다.
즉, 복합컬럼 인덱스 중 emp_no
컬럼만을 사용했음을 나타낸다.
Equal 비교 조건으로 어떤 값이 제공되었는지 표시해준다.
일반적으로 이 컬럼은 크게 신경쓰지 않아도 되는데, 컬럼에 func
라고 표시될때는 조금 주의해서 살펴봐야 한다.
이는 Function
의 줄임말으로 값을 그대로 사용한게 아니라 변환이나 연산을 거친 뒤 값을 사용했다는 뜻이다.
1 | EXPLAIN |
근데 중요한 점은, 위처럼 명시적으로 변환할 때 뿐만 아니라 MySQL 서버가 내부적으로 값을 변경할떄도 func
가 출력된다는 점이다.
타입이 일치하지 않는 두 컬럼을 비교할때가 대표적이다.
가능하다면 이런 내부 연산이 발생하지 않도록 타입을 맞춰주는 것이 좋다.
해당 쿼리를 처리하기 위해 얼마나 많은 레코드를 디스크로부터 읽고 체크해야 하는지를 의미한다.
이는 통계 정보를 참조해 옵티마이저가 산출한 값이라서 정확하지는 않다.
아래는 rows
컬럼이 실행계획에 영향을 끼친 예시이다.
1 | EXPLAIN |
보다시피 해당 쿼리를 처리하기 위해서는 331,143
개의 레코드를 읽어야 한다고 예측했다.
하지만 dept_emp
테이블의 전체 레코드 개수가 331,603
개로, 거의 차이가 나지 않는다.
그래서 옵티마이저는 풀 테이블 스캔이 낫다고 판단하여 ALL
로 처리된것을 볼 수 있다.
1 | EXPLAIN |
예측되는 rows
를 줄였을 경우 range
가 출력됨을 볼 수 있다.
이름과는 달리 실행계획에서 성능에 중요한 내용이 여기 자주 표시된다.
여기에 표출되는 고정된 몇개의 문장들이 있고, 일반적으로 2~3개씩 같이 표시된다.
아래는 departments
테이블과 dept_emp
테이블에 모두 존재하는 dept_no
을 중복없이 가져오기 위한 쿼리이다.
1 | EXPLAIN |
위처럼 Distinct
가 출력되면 실제로 아래와 같이 효율적으로 처리됨을 의미한다.
DISTINCT 처리를 위해 조인하지 않아도 되는 항목은 무시하고 꼭 필요한 레코드만 읽고 있다.
col1 IN(SELECT col2 FROM ...)
형태의 쿼리에서 자주 발생할 수 있는 형태이다.
만약 col1
의 값이 NULL
이 된다면 결과적으로 NULL IN(SELECT col2 FROM ...)
의 형태가 되게 되는데,
이 때 서브쿼리에 대해 풀 테이블 스캔
이 발생하게 되고(이유를 정확히 모르겠다…),
이로 인해 상당한 성능저하가 발생하게 된다.
즉 이 메세지는 col1
이 NULL
을 만나면 풀 테이블 스캔
을 사용할 것이라고 알려주는 키워드인 것이다.
만약 col1
이 NOT NULL
로 정의되었다면 이 메세지는 표시되지 않을 것이다.
HAVING
절의 조건을 만족하는 레코드가 없을 때 표시된다.
쿼리를 잘못 작성한 경우가 대부분이지만, 실제 저장된 데이터 때문에 발생하는 경우도 종종 있다.
쿼리와 데이터를 다시 확인해 보는것이 좋다.
WHERE
절의 조건이 항상 FALSE
가 될 수 밖에 없을 때 표시된다.
WHERE
절의 조건이 항상 FALSE
가 될 수 밖에 없는데, 테이블을 읽어본 뒤
알았다는 의미이다.
아래와 같은 쿼리가 이에 해당한다.
1 | EXPLAIN |
이를 통해 실행계획을 만드는 과정에서 옵티마이저가 쿼리의 일부분을 실행해 본다는 사실을 알 수 있다.
MIN()
이나 MAX()
와 같은 집합 함수가 있는 쿼리의 조건절에 일치하는 레코드가 하나도 없을 때 표시된다.
1 | EXPLAIN |
FROM
절 자체가 없거나, 상수 테이블을 의미하는 DUAL
테이블을 사용할 때 표시된다.
Outer Join
을 이용해서 Anti-Join
을 수행할 경우 표시된다.
Anti-Join
A 테이블에는 존재하지만 B 테이블에는 존재하지 않는 값을 조회할 떄 사용하는 기법이다.
일반적으로NOT IN
,NOT EXIST
,Outer Join
을 통해서 처리하는데 레코드의 건수가 많을 때는Outer Join
이 빠르다.
1 | EXPLAIN |
다음과 같은 쿼리가 있다고 하자.
1 | EXPLAIN |
레코드를 하나씩 읽을 때 마다 e1.emp_no
의 값이 변경되기 때문에 옵티마이저 입장에서는 e2
를 인덱스 레인지 스캔으로 읽을지, 풀 테이블 스캔으로 읽을 지 판단하지 못한다.
만약 employees
테이블의 레코드가 1억건이라고 가정했을때,
e1.emp_no
이 1
인 경우는 e2
의 모든 레코드를 읽어야하지만, e1.emp_no
이 100,000,000
인 경우에는 e2
의 레코드를 1건만 읽으면 된다.
즉 e1.emp_no
이 작은 값일 때는 풀 테이블 스캔이 좋고, 큰 값일 때는 인덱스 레인지 스캔이 좋다.
이 현상을 줄여서 얘기하면 매 레코드 마다 인덱스 레인지 스캔을 체크한다 라고 할 수 있는데, 이게 바로 Range checked for each record
문구인것이다.
참고로 이 문구가 표시될 때 ref
컬럼의 값이 ALL
로 표출되는데, 이는 인덱스 사용여부를 검토하고 풀 테이블 스캔을 할 수 있기 때문에 ALL
로 표시된것이지 실제로 풀 테이블 스캔을 의미하는 것은 아니다.
그리고 뒤에 (index map: N)
이라는 문구가 추가로 출력되는데, 이는 사용할 인덱스의 후보를 나타내준다.
만약 (index map: 0x19)
라는 문구가 표시되었다고 가정하자.
일단 16진수인 0x19
를 2진수로 변경해줘야 한다. 11001
이 된다.
그리고 생성된 인덱스의 순서를 보기위해 SHOW CREATE TABLE table_name
을 입력한다.
그러면 테이블이 가지고 있는 인덱스를 순서대로 확인이 가능한데, 이 순서를 위의 2진수로 체크하면 된다.
즉 현재 11001
이라는 값을 얻었으므로, 1번째
, 2번째
, 5번째
순서에 나열된 인덱스가 사용후보가 된다는 것이다.
매 레코드를 돌면서 위 3개의 사용후보들 가운데 어떤 인덱스를 사용할지 결정하게 되는데, 실제로 어떤 인덱스를 사용했는지는 알 수 없다.
MySQL에는 서버 내에 존재하는 DB의 메타정보를 담은 INFORMATION_SCHEMA
라는 DB가 제공되는데, 이 데이터를 읽었을 떄 표시된다.
MIN()
이나 MAX()
사용 시 인덱스를 다 읽지 않고 오름차순 또는 내림차순으로 1건만 읽는 형태의 최적화가 적용될 때 표시된다.
1 | EXPLAIN |
emp_no
과 from_date
로 복합 프라이머리 키가 설정되어 있으므로 emp_no
이 10001
인 레코드를 찾은 뒤 첫 행과 마지막 행만을 읽으면 된다.
Not exists
의 반대라고 볼 수도 있겠다.
Outer Join
을 수행하는 쿼리에서 아우터 테이블에 조건 조건에 일치하는 레코드가 없을 때 표시된다.
ORDER BY
가 사용된 쿼리에서만 나타날 수 있다.
ORDER BY
처리에 인덱스를 사용하지 못했을 경우 나타난다.
인덱스를 사용하지 못할 경우 조회된 레코드를 메모리 버퍼에 복사한 뒤 퀵 소트 알고리즘을 수행한다.
이는 많은 부하를 일으키므로 가능하면 쿼리를 튜닝하거나 인덱스를 생성하는 것이 좋다.
데이터 파일을 전혀 읽지 않고 인덱스만 읽어서 쿼리를 처리할 수 있을 떄 표시된다.
참고로 InnoDB의 테이블은 모두 클러스터링 인덱스로 구성되어 있어서 모든 보조 인덱스들은 데이터의 레코드 주소 값으로 프라이머리 키 값을 가진다.
즉 아래와 같은 쿼리도 Using Index
로 처리되는 효과를 낼 수 있다.
1 | EXPLAIN |
현재 first_name
컬럼에 대해서만 인덱스가 생성되어 있지만 클러스터링 인덱스의 특징으로 Using index
로 처리 가능하게 된다.
GROUP BY
처리를 위해서는 그룹핑 기준 컬럼을 이용해 정렬 작업을 수행한 뒤 그 결과를 그룹핑하는 고부하 작업을 필요로 한다.
하지만 GROUP BY
처리에 인덱스를 사용할 수 있으면 정렬된 인덱스 컬럼을 읽으면서 그룹핑 작업만 수행하면 된다.
이는 상당히 빠르게 처리되고, 이처럼 GROUP BY
에 인덱스가 사용되었을 때 이 메세지가 표시된다.
일반적으로 조인이 되는 컬럼은 인덱스를 생성해야 빠른 처리를 할 수 있다.
이는 MySQL이 조인을 Nested loop
방식으로만 처리하기 때문이다.
Nested loop join
FROM
절에 아무리 테이블이 많아도 조인을 수행할 때 반드시 2개의 테이블이 비교되는 방식으로 처리하는 것이다.
먼저 읽히는 테이블이드라이빙 테이블
이 되고, 뒤에 읽히는 테이블이드리븐 테이블
이 된다.
즉 드라이빙 테이블의 건수만큼 드리븐 테이블이 스캔되므로 드라이빙 테이블이 어떤 테이블이냐가 성능을 많이 좌우한다.
옵티마이저는 두 테이블을 조인할때 각 테이블의 조인 기준 컬럼에 인덱스가 있는지 조사하고, 인덱스가 없는 테이블이 있다면 그 테이블을 드라이빙 테이블로 지정하여 실행한다.
위에서 언급했듯이 드리븐 테이블은 계속해서 탐색되므로 인덱스가 없으면 성능에 영향을 많이 미치기 때문이다.
근데 만약 드리븐 테이블에도 인덱스가 없다면 매번 드리븐 테이블을 풀 테이블 스캔해야 하는데, MySQL에서는 이러한 비효율적인 검색을 보안하기 위해 조인 버퍼
라는 것을 사용한다.
드라이빙 테이블에서 읽은 데이터를 조인 버퍼에 저장해두고, 필요할 때 마다 재사용할 수 있게 해준다.
조인 버퍼가 사용되는 실행계획에 Using join buffer
메세지가 표시된다.
index_merge
방식이 사용될 때 두 인덱스의 결과를 어떻게 병합했는지 조금 더 상세하게 설명하기 위한 메세지이다.
출력되는 메세지는 아래의 3개이다.
AND
로 연결될 때OR
로 연결될 때Using union
으로 처리하기 힘들 정도로 대량의 조건들이 OR
로 연결될 때MySQL은 쿼리를 처리하는 동안 중간 결과를 담아두기 위해 임시 테이블을 사용한다.
이 메세지가 표시되면 임시 테이블을 사용했다는 의미인데, 사용된 임시테이블이 메모리에 생성되었는지 디스크에 생성되었는지는 알 수 없다.
Using where
메세지는 MySQL 엔진에서 별도의 가공을 해서 필터링 작업을 거쳤을 경우 표시된다.
실제로 가장 흔하게 표시되는 메세지이다.
그러나 이 메세지만으로 정확하게 성능 이슈를 판단하긴 어렵고, MySQL 5.1부터 추가된 Filtered
컬럼과 함께 보아야 성능샹 이슈를 쉽게 체크할 수 있다.
스토리지 엔진에서 받은 레코드가 MySQL 엔진을 거친 뒤 얼마나 남았는가를 체크해줄 수 있는 컬럼이다.
일반 실행계획에선 볼 수 없고, EXPLAIN EXTENDED
라는 명령어를 사용해야 한다.
1 | EXPLAIN EXTENDED |
현재는 스토리지 엔진에서 전달받은 299,113건의 레코드에 대해 MySQL 엔진에서 필터링 된 것 없이 100% 출력되고 있음을 볼 수 있다.
EXPLAIN EXTENDED의 추가 기능
EXPLAIN EXTENDED
를 실행한 뒤SHOW WARNINGS
명령을 실행하면 옵티마이저가 다시 재조합한 쿼리 문장을 확인 가능하다.
옵티마이저가 어떻게 쿼리를 해석하고 변환했는지 직접 확인할 수 있으므로 알아두면 도움이 된다.
스토리지 엔진과 mysql 엔진에서 읽어오는 데이터 양 차이의 기준은 뭘까?
데이터 모델링은 DBMS 사용에 가장 중요한 부분이면서 가장 쉽게 간과되는 부분이기도 하다.
데이터 모델링은 크게 논리 모델링
과 물리 모델링
으로 나눌 수 있다.
제대로 표현되고 있는 곳은 잘 없지만,
원래 논리 모델링과 물리 모델링의 차이는 테이블/칼럼 등의 이름이 영어냐 한글이냐가 아니라, 모델에 표현하려는 것이 업무냐 시스템이냐의 차이다.
업무를 분석하여 그에 대한 데이터 집합/관계를 중점적으로 표현하는 것이 논리 모델링
이고,
그 산출물을 시스템으로 어떻게 표현할지 고려하는 것을 물리 모델링
이라고 볼 수 있다.
ERD상에 표현되는 오브젝트는 논리 모델링이냐 물리 모델링이냐에 따라 각각 이름이 다르게 표현된다.
논리 모델 | 물리 모델 |
---|---|
엔티티(Entity) | 테이블(Table) |
속성, 어트리뷰트(Attribute) | 컬럼(Column) |
관계, 릴레이션(Relation) | 관계, 릴레이션(Relation) |
키 그룹(Key group) | 인덱스(Index) |
객체지향 언어에서 클래스와 동급의 의미다.
일반적으로 2개 이상의 속성을 가지고, 1개 이상의 레코드를 가지는 오브젝트를 말한다.
엔티티를 도출할 때 가장 중요한 것은 용어의 정의다.
해당 용어가 의미하는 범위가 어디까지인지 명확히 하고, 그에 걸맞는 이름을 부여해야 한다.
그래야만 다음으로 도출할 속성이나 식별자, 관계가 명확해질 수 있다.
ERD 에서 엔티티는 이와 같이 표현한다
박스 외부에는 엔티티의 이름을 적고, 박스 내부 상단에는 PK, 하단에는 일반 속성을 나열한다
엔티티는 크게 키 엔티티
, 메인 엔티티
, 액션 엔티티
로 구분할 수 있다.
키 엔티티
는 대상 데이터 중 가장 최상위에 존재하는 엔티티이며, 일반적으로 메인 엔티티와 액션 엔티티를 만들어내는 부모 역할을 한다
일반적으로 현실에 존재하는 객체를 표현하는 경우가 많다. 사원, 고객, 상품 등의 엔티티는 대표적인 키 엔티티이다.
이러한 키 엔티티
간의 작용으로 만들어지는 엔티티를 액션 엔티티
라고 한다
예로는 구매, 계약 등이 있다
이러한 액션 엔티티들 중 서비스에서 상당히 중요한 역할을 하는 엔티티들을 메인 엔티티
라고 한다
위에서 언급한 구매, 계약 등은 사실상 메인 엔티티의 대표적인 예시이다
액션 엔티티도 나중에 업무가 변화하고 확장되면 메인 엔티티로 향상될 수 있다.
엔티티의 이름은 복수형 표현을 사용하지 않고 별도의 수식어가 없는 단순 또는 복합 명사 형태를 사용한다.
엔티티가 가지고 있는 속성으로써, 더 이상 분리될 수 없는 최소의 데이터 보관 단위이다.
어트리뷰트는 반드시 독자적인 성질을 가지는 하나의 값만을 저장해야 한다.
그런데 값의 최소 단위(하나의 값)라는 것이, 표현되는 서비스에 따라 달라질 수 있다.
예를 들면 주소가 있다
주소를 시군구, 읍면동 단위로 조작하는 행위가 많다면 속성이 시, 군, 구 등으로 잘개 쪼개지겠지만, 그렇지 않다면 굳이 잘게 쪼개어 관리를 어렵게 할 이유는 없다.
위와 같은 이유가 아닌 상태에서 어트리뷰트에 여러 값을 저장하는 행위는 지양해야 한다.
예를 들면 회원 취미 정보를 하나의 어트리뷰트에 구분자로 한꺼번에 저장하는 경우이다.
이는 어트리뷰트의 원자성에 위배되며 물리 모델링(성능)에 나쁜 영향을 미칠 가능성이 크다.
어트리뷰트는 그 이름 자체만으로 그 의미를 이해할 수 있게 작명하는 것이 좋다.
사람들은 대부분 어트리뷰트의 이름을 최대한 간단히 작명하려는 경향이 있는데, 이름을 너무 간략히 작성하면 나중에 그 의미를 혼동하기 쉽다.
아래는 잘못된 어트리뷰트 명명의 예시이다.
속성을 하나씩 살펴보자.
범위를 한정하는 한정자
와 값을 표현하는 명사
로 구성해줘야 한다.번호
의 경우 값을 표현하는 명사
만 사용되어 가독성이 떨어진다. 누군가는 이를 회원번호가 아니라 전화번호라고 생각할 수도 있다.사무실 주소
라는 어트리뷰트가 추가로 있다면, 이 어트리뷰트가 자택 주소를 나타낸다는 보장이 없어진다.번호
와는 반대로 범위를 한정하는 한정자
만 사용된 케이스이다.1번에서 언급하였듯이 어트리뷰트의 이름은 범위를 한정하는 한정자 + 값을 표현하는 명사로 구성하는 것이 가장 이상적이다.
어트리뷰트의 이름이 너무 길어지면 물리모델링 과정에서 까다로워지므로 단어 2~4개 정도를 결합해서 사용하는 것이 좋다.
식별자는 본질 식별자
와 실질 식별자
로 나눌 수 있다.
본질 식별자는 엔티티의 레코드가 생성될 수 있는 조건을 알려주는 식별자를 의미한다.
위의 구매
테이블에서 보이듯이, 고객아이디 + 상품코드 + 구매일자
가 있어야 레코드가 한건 생성될 수 있으므로, 이 3개의 어트리뷰트가 묶여서 본질 식별자가 되는 것이다
실질 식별자
란 실질적으로 테이블에서 식별자로 사용하고 있는 값을 의미하느데, 구매
테이블의 경우 본질 식별자를 그대로 실질 식별자로 사용하고 있다
근데 여기서 문제가 되는게, 보통 구매 엔티티의 경우 주문이력, 상태변화 등 수많은 자식 엔티티를 가질 가능성이 상당히 높다.
근데 현재와 같이 본질 식별자를 식별자로 사용할 경우, 자식 엔티티의 경우 부담해야하는 어트리뷰트의 개수가 계속해서 많아지게 된다.
그러므로 인위적인 값을 생성하여 이를 실질 실별자로 사용하는 경우가 많다.
이때 사용되는 식별자를 인조 식별자
라고 한다.
엔티티간 상호작용을 표현해주는 것을 말한다.
엔티티와 동일하게 매우 중요한 역할을 수행한다. 관계없이 엔티티만 있는 ERD는 ERD로 볼 수 없다.
부모 엔티티의 식별자가 자식 엔티티의 식별자로 포함될 경우 식별 관계
, 그냥 일반 어트리뷰트로 포함될 경우 비식별 관계
라고 한다.
부모 엔티티가 자식 엔티티를 만들어 내는데 필수적인 역할을 하고 있을 경우 식별 관계를 형성할 대상이 될 수 있다.
하지만 그 대상을 모두 식별 관계로 형성하면 자식의 식별자가 너무 많아지므로 관계 중 유일성을 보장할 수 있는 최소한의 대표 관계만 식별 관계로 선택하고, 나머지는 비식별 관계로 선택하는 것이 좋다.
부모 엔티티의 레코드 하나에 자식 엔티티의 레코드가 얼마나 만들어질 수 있는지를 의미한다.
정확히 몇 건이냐를 표시하는 것이 아니라, 0건, 1건, N건(1건 이상)
으로 구분해서 표시한다.
기수성은 관계선의 양쪽 끝에 표시하며, 나타내는 법은 아래와 같다.
(이 그림 하나 그린다고 30분을 넘게 썼다… 역시 난 미술이랑 안맞다 ㅋㅋㅋ)
그리고 아래는 실제 표시되는 형태이다.
아래는 간단한 예시이다.
한명의 회원은 한번도 구매를 하지 않을수도, 1번 이상 구매할 수도 있음을 나타낸다.
모델링에는 수많은 관계들이 나타나겠지만, 대표적으로 많이 나타나는 몇개의 패턴들이 있다.
계층 관계
부모와 자식간의 직선적인 관계가 연속되는 형태를 말한다.
자식 엔티티로 갈수록 식별자의 개수가 많아지므로, 적절한 수준에서 식별자를 인조 식별자로 대체하는 것이 좋다.
업무에 따라 다르지만 보통은 2~4단계에서 대체하는 것이 일반적이다.
순환 관계
하나의 엔티티가 부모임과 동시에 자식이 되는 재귀적인 형태를 말한다.
MySQL은 Oracle과 달리 재귀 쿼리가 지원되지 않는데, 이를 이유로 순환 관계를 피하는 모델링은 잘못된 방식이다.
그러한 이유로 순환 관계를 계층 관계로 풀어봤자 나아지는 것은 아무것도 없고, 결국에 더 복잡해질 뿐이다.
N:N 관계
보통의 데이터 모델에서는 1:N 관계가 90% 정도를 차지할 정도로 많이 존재히나, 가끔씩 N:N(다대다) 관계도 등장한다.
아래는 N:N 관계의 대표적인 예시다.
학생은 여러개의 과목을 수강할 수 있고, 과목은 여러 학생에 의해 수강될 수 있다
그러나 이런 표기법은 논리모델에서나 가능하고, 물리모델에서는 불가능하다.
즉 물리모델에서는 다른 방식으로 풀어야한다.
이처럼 2개의 1:N 관계로 풀어줘야 한다. 이를 N:N 관계 해소
라고 한다.
수강
과 같은 엔티티를 관계 엔티티
라고 표현한다.
이는 요즘에 유행하는 SNS의 팔로잉, 팔로워의 대표적인 예시이다.
ERD를 작성하다 보면 엔티티를 구성하는 어트리뷰트와 관계가 비슷한 엔티티를 자주 보게 된다.
관계가 비슷하다는 것은 용도가 비슷하다는 의미인데, 이런 엔티티는 통합의 대상이 아닌지 주의깊게 살펴보는 것이 좋다.
아래는 통합의 간단한 예시다.
법인고객과 개인고객이 서로 많은 어트리뷰트를 공유하고 있어 이를 하나의 엔티티로 합친 예시이다.
만약 두 엔티티의 어트리뷰트 차이가 꽤나 난다면, 둘의 공통 속성을 모아서 하나의 통합 엔티티로 만드는 방법도 고려해볼 수 있다.
또한 이 서비스에서 어떤식으로 엔티티나 어트리뷰트에 접근하게 될 지도 고려하면서 통합이나 분리를 선택하는 것이 가장 좋다.
관계 또한 통합하는 과정을 거치는 것이 좋다.
왼쪽 관계의 경우 고객의 수가 늘어나는 등의 요구사항에 대응하기 어려우므로, 오른쪽과 같이 고객들을 별도의 엔티티로 분리해주는 것이 좋다.
참고로 관계를 통합하면 조인이나 저장되는 테이블이 늘어나서 개발이 번거로워질수 있다.
하지만 대체로 관계의 통합은 성능적 이슈보다는 업무에 유연하게 대응하기 위한 것이다.
만약 데이터를 하나의 테이블에 다 때려넣으면 어떻게 될까?
불필요한 공간 낭비는 기본이고 사람이 관리하기도 매우 힘들것이며
삽입이상, 갱신이상, 삭제이상과 같은 부작용 또한 초래할 수 있게 된다
이상현상(abnomaly) : https://yaboong.github.io/database/2018/03/09/database-anomaly-and-functional-dependency/
정규화란 중복과 이상현상이 발생하지 않도록 데이터를 적절한 기준으로 나눠서 저장하는 것을 말한다
객체지향 프로그래밍에서 중복을 제거하기 위해 객체들의 관심사를 분리하는 과정과 비슷하다고 보면 된다
(데이터베이스에서 관심사를 발생하지 않아 발생하는 문제의 경우 코드보다 훨씬 치명적이다)
제1정규화의 요건은 모든 속성은 반드시 하나의 값을 가져야 한다
이다.
아래와 같이 하나의 어트리뷰트에 여러개의 값을 저장하거나, 하나의 엔티티에서 똑같은 성격의 어트리뷰트가 여러번 나열되는 것은 제1정규화를 위반한 것이다.
제2정규화의 요건은 식별자 일부에 종속되는 어트리뷰트는 제거해야 한다
이다.
엔티티의 식별자를 구성하는 어트리뷰트가 2개일떄, 그 엔티티의 모든 어트리뷰트가 식별자 모두에 완전하게 종속적이어야 한다.
친구회원명
이라는 어트리뷰트는 식별자 중 친구회원번호
에만 종속관계를 가진다.
이는 제2정규화를 위반한 것이다
제3정규화의 요건은 식별자 이외의 속성간에 종속관계가 존재하면 안된다
이다.
직업명
은 직업코드
에 종속적인 어트리뷰트이므로, 따로 엔티티로 분리하며 제거하였다.
논리 모델링을 통해 나온 산출물을 RDBMS의 특성에 맞게 변환하는 작업이다.
물리 모델링에서는 인덱스
라는 존재 때문에 프라이머리 키 선택을 더욱 신중하게 해야 한다.
앞의 논리 모델링에서도 고려했던 인조 식별자
사용 여부를 물리 모델링에서도 고려하게 되는데, 여기서 인덱스의 성능까지 추가적으로 생각해줘야 한다.
InnoDB
는 프라이머리키에 의해 클러스터링 되는 스토리지 엔진인데, 이러한 엔진에서는 프라이머리 키를 레코드의 주소 대신 사용하기 때문에 프라이머리키를 구성하는 컬럼의 개수가 많아지면 성능적으로 이슈가 발생할 수 있다
알다시피 인덱스는 레코드의 주소를 가지고 있고, 이 말인 즉 프라이머리 키가 많을 경우 인덱스의 크기도 같이 비대해짐을 의미한다
인덱스의 크기가 작을 경우 차이가 미비하지만, 크기가 커질 경우 그 차이가 확연하게 드러나게 된다.
디스크를 차지하는 크기가 커진다는 것은 그만큼 많은 디스크 입출력을 필요로 하고, 메모리에 캐시나 버퍼링을 하기 위해 더 많은 물리적 메모리가 필요하다는 것을 의미한다.
그러므로 프라이머리 키는 적절한 선에서 인조 식별자를 선택해주는 것이 좋다
그리고 추가로, 프라이머리 키 또한 인덱스로 사용되므로 반드시 SELECT의 조건절에 자주 사용되는 컬럼 위주로 순서를 배치해줘야 한다
물리 모델링에서 칼럼의 데이터 타입은 가능한 한 최소 단위의 타입을 부여해야 한다.
레코드의 개수가 많아지면 데이터 타입 한 바이트라도 많은 차이를 만들어내기 때문이다.
데이터의 타입은 저장하려는 데이터의 성격 그대로 타입을 선정하는 것이 가장 좋다.
숫자나 날짜 데이터를 모두 문자열 칼럼에 저장해도 아무런 차이가 없다면 처음부터 MySQL에 이렇게 많은 데이터 타입이 제공되지 않았을 것이다.
만약 저장할 데이터 타입이 명확하지 않고 두개의 데이터 타입 중간쯤 위치해 애매하다면, 그 두 데이터 타입의 장단점을 비교해 선택하는 것이 좋다.
대표적 예시로는 IP주소가 있다.
항상 우리는 칼럼에 저장될 데이터의 최대 길이만을 생각하여 길이를 지정하는 경향이 있는데, 이것보다는 우리가 저장할 데이터가 어떤 특성을 가지느냐에 따라 칼럼의 길이를 결정해야 한다.
예를 들어 URL 데이터를 저장하는 칼럼의 경우,
URL의 최대 길이에 집중할 것이 아니라 우리 서비스에 필요한 URL 길이만을 생각하면 된다.
그리고 인조식별자에 INTEGER
대신 BIGINT
를 사용하는 것도 어떻게 보면 너무 과도하게 멀리 생각하는 행위라고 볼수도 있다.
문자열 타입에서 문자열이 어떤 문자집합을 가지는지도 상당히 중요한 문제이다.
특별히 지정하지 않으면 MySQL 서버의 default-character-set
에 지정된 문자집합을 사용한다.
하지만 명확한 기준이 있다면 latin1
, utf8
를 같이 사용하여 데이터 저장 공간을 줄이는 것도 좋다.
컬럼의 길이나 문자집합을 신경쓰는 이유는 데이터가 디스크를 많이 사용하는 것을 막기 위함이다.
MySQL에서는 임시테이블/버퍼작업(정렬, 그룹핑 등)을 위해 별도의 메모리 할당이 필요하다.
이때 MySQL 서버는 실제 저장된 데이터 길이로 메모리를 할당하는 것이 아니라, 데이터 타입에 명시된 길이를 기준으로 메모리를 할당하고 사용한다.
그런데 이 메모리 공간이 일정 크기 이상을 초과하면 메모리가 아니라 디스크에서 처리된다.
즉, 테이블 컬럼이 과도하게 크게 설정되면 메모리로 처리되어야 할 것이 디스크로 처리될 수도 있다.
InnoDB
의 경우 NULL
이 저장되는 칼럼은 전혀 디스크 공간을 사용하지 않는 특징이 있다.
즉 NULL
을 저장함으로써 디스크 공간을 줄일 수 있는 것이다.
하지만 SELECT
가 많이 발생하는 컬럼의 경우 NULL
을 저장하는 행위를 지양하는 것이 좋다.
MySQL에서 NULLABLE
컬럼에 IN 연산
등을 했을 경우 굉장히 이상한 비교 작업을 내부적으로 하기 때문이다.
정규화는 데이터의 저장 비용을 최소화하는 역할을 담당하기 때문에,
진행할수록 테이블의 개수나 컬럼의 개수가 증가하게 되고, 이는 결국 SELECT
의 부담으로 이어진다.
반정규화는 COUNT
나 GROUP BY
같은 컬럼들을 미리 집계하여 별도로 저장하여 데이터를 읽어오는 비용을 최소화하는 작업을 말한다.
조인을 없애기 위해 원본 컬럼의 값을 변경하지 않고 그대로 다른 테이블로 복사해두는 형태를 말한다.
복사해온 컬럼을 이용해 GROUP BY
나 ORDER BY
를 인덱스로 할수있다면 성능에 상당히 도움이 될 수 있다.
하지만 복사해온 컬럼이 자주 변경된다면 비효율적인 작업이 될 것이다.
그러므로 읽기와 변경의 비율을 따져보고 컬럼 복사를 진행하는 것이 좋다.
어떠한 계산의 결과로 만들어진 값을 저장해두는 컬럼을 의미한다.
대부분 여러 레코드의 건수, 최대값, 최소값등을 미리 계산해서 저장해두는데 사용한다.
계산된 결과가 빈번히 호출되고, 매번 계산하기가 부담스러울 때 사용하면 좋은 방법이다.
하지만 계산의 결과가 빈번히 변경된다면 문제가 될수있다.
이처럼 잦은 데이터의 변경은 문제가 될 수 있으므로, 20-30분에 한번씩 도는 배치를 통해 계산 컬럼을 변경해주는 것이 가장 효율적인 방법이다.
해시 인덱스는 칼럼의 원래 값을 인덱싱하는 것이 아니라, 길이를 훨씬 줄인 해시값으로 인덱스를 구성하는 방식이다.
하지만 MyISAM이나 InnoDB에서는 이를 지원하지 않으므로, MD5 함수를 통해 이를 흉내내야 한다.
방식은 간단하다. 별도의 해시 저장용 컬럼을 만들고 그곳에 인덱스를 생성하는 것이다.
그리곤 아래와 같이 사용할 수 있다.
1 | SELECT * |
url에 직접 인덱스를 생성할 필요도 없고, 만약 url컬럼에 인덱스를 생성하지 못하는 경우에도 유용하게 사용할 수 있다.
기본적으로 InnoDB
는 767바이트
까지만 인덱스를 생성할 수 있기 때문이다.
1 | # -A 또는 --all-databases 옵션 |
1 | # 특정 database |
데이터 없이 테이블 구조(스키마)만 받을 때 사용한다
1 | # -d 또는 --no-data 옵션 |
특정 database의 특정 table에서 원하는 값만 덤프받고 싶을 경우 사용한다.
ex) test
db의 employees
테이블에서 emp_no
이 1
이상 10
이하인 값만 덤프를 받고자 할 때
1 | # -w 옵션을 사용한다. 조건은 ''로 묶어줘야 한다 |
--column-statistics=0
옵션을 추가해주면 됨1 | mysql -u[아이디] -p[패스워드] [데이터베이스명] < 덤프파일명.sql |
덤프파일내에 database 생성 구문이 있을 경우 지정된 데이터베이스는 무시된다.
윈도우 키(대시)
https://reachlabkr.wordpress.com/2014/12/06/ubuntu-에서-supercommand-key-shortcut-해제/
compiz 설치후에
ubuntu unity plugin
launcher에 젤 상단 dash 부분 사용하지 않음으로 변경
난 super space로 바꿨는데, mac spotlight 느낌이 난다
윈도우 w키(창 닫기)
창 관리 - 스케일에 가면 있음. super + tab 으로 바꾼건 윈도우 같다고 함
파라미터 보기
https://askubuntu.com/questions/68463/how-to-disable-global-super-p-shortcut
sudo apt-get install dconf-tools
dconf-editor
위처럼 따라간 뒤 xrandr 해제
근데 … 좀 늦게 눌러야 인식됨 ㅡㅡ
셋 다 문자열 처리를 위한 클래스이나, 그 처리 방법에서 차이를 보인다.
실제로 사용하는 상황에 따라 성능차이가 발생하니 이를 확실히 정리하고자 한다.
String
은 기본적으로 변경이 불가능한 immutable
클래스이다.
이말인 즉 String
에 추가적인 연산을 하게 될 경우, 기존 String
클래스의 값이 변경되는 것이 아니라 항상 새로운 클래스가 생성된다는 뜻이다.
1 | String str1 = "AAA" + "BBB"; |
str1
의 경우 힙 영역에 AAA
, BBB
, AAABBB
가 각각 생기게 되는 것이고,
str2
의 경우 힙 영역에 CCC
, DDD
, CCCDDD
가 생기게 되는 것이다.
String
이 가지고 있는 각종 mutable
해 보이는 연산(substring()
, toLowerCase()
) 등 은 모두 위와 같이 처리된다.
JDK 5.0 미만 버전 한정이다. JDK 5.0 이상부터는
String
연산도 내부적으로StringBuilder
로 변환된다.
String
을 이렇게 디자인 한 이유는 프로그램 기본 문자열 클래스로 사용하기 위해서이다.
프로그램 작성 시 문자열을 생성하고 참조하는 경우는 많으나, 변경하는 일은 그리 많지 않다.
이럴 경우 위와 같은 immutable
형태로 선언하게 되면 많은 효과를 누릴 수 있다.
일단 thread-safe
하기 때문에 여러 쓰레드 내에서 자유롭게 참조할 수 있고,
한번 생성한 문자열은 같은 문자열에 대해서는 변경을 가하지 않으므로,
요청 시 똑같은 주소값을 반환해 주기 때문에 성능상으로 많은 이점을 누릴 수 있다.
실제로 아래의 연산이 성립한다.
1 | String str1 = "AAA"; |
두 문자열이 같은 힙 영역을 공유하기 때문이다.
이에 반해 StringBuffer
클래스는 mutable
클래스이다.
위처럼 기존의 문자열에 추가적인 연산을 하게 되면, String
처럼 새로운 문자열이 생성되는 것이 아니라 기존의 문자열에 추가적인 연산을 하게 된다.
그러므로 당근 문자열의 연산이 많을 경우에는 String
클래스보다 훨씬 효율적이다.
그러면 아! 그럼 문자열 연산이 많을 때는 무조건 StringBuffer 써야겠구나!
라고 할 수 있지만,
StringBuffer
는 thread-safe
를 위해 내부적으로 synchronized
연산을 수행하게 되므로 문자열의 변경이 잦지 않을 경우는 String
보다 나쁜 성능을 보인다.
그러므로 연산이 많을 경우에 사용하도록 하자~~
JDK 5.0 부터 나왔다.
StringBuffer
와 동일하나 thread-safe
하지 않다는 점이 차이점이다.
위에서 언급했듯이 JDK 5.0 이후로는 String
연산이 내부적으로 이 StringBuilder
로 변환되어 처리된다.
문자열 연산이 잦지 않은 경우는 String
을 사용하는 것이 좋고,
연산이 잦을 경우에는 thread-safe
여부를 따져서 StringBuffer
나 StringBuilder
를 사용하면 된다.
어쩌피 JDK 5.0 이후로 연산 시 String
이 StringBuilder
로 변환된다지만, 문자열을 더할 떄 까지 객체를 계속 추가해야 한다는 사실은 변함이 없으므로, 연산이 많으면 StringBuilder
나 StringBuffer
를 사용하는 것이 좋다.
1 | Scanner sc = new Scanner(System.in); |
1 | BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); |
문자열에 최적화 된 BufferedReader
에 비해 Scanner
는 다양한 기능을 지원하므로 속도가 조금 더 느리다.
1 | BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); |
1 | BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); |
문자열을 잘라 쓰는건 똑같은 맥락이나, split
은 정규식을 기반으로 자르는 로직이므로 내부가 복잡하다.
그에 반면 StringTokenizer
의 경우 단순히 공백을 땡기는 것이므로
정규식 처리가 딱히 필요한게 아닌 경우 StringTokenizer
가 효율적이다.
메서드에 @ModelAttribute
를 파라미터로 선언했을 경우 처리되는 과정은 다음과 같다.
파라미터 타입의 오브젝트를 새로 만든다. 때문에 디폴트 생성자가 필수로 필요하다.
@SessionAttributes
를 통해 저장된 오브젝트가 있으면 새로 만들지 않고 세션에서 가져온다.
HTTP 요청을 생성(혹은 가져온) 오브젝트 프로퍼티에 바인딩 해준다.
이 과정에서 각 프로퍼티에 맞게 타입을 변환해준다.
만약 타입 변환 오류가 발생할 시 BindingResult
오브젝트에 오류를 저장해서 컨트롤러로 넘겨준다.
검증작업을 수행한다. 2번의 과정에서 타입에 대한 검증은 이미 끝냈고, 그 외의 검증은 검증기를 통해 등록할 수 있다.
프로퍼티 바인딩이란 오브젝트의 프로퍼티에 값을 넣는 행위를 말한다.
프로퍼티에 맞게 타입을 적절히 변환하고 해당 프로퍼티의 수정자 메서드를 호출하는 것이다.
스프링에선 크게 두가지의 프로퍼티 바인딩을 지원하는데
첫번째는 애플리케이션 컨텍스트 XML 설정파일로 빈을 정의할 때 사용했던 <property>
태그이다.
이 태그를 통해 빈의 프로퍼티에 값을 주입했었다.
두번째는 HTTP 요청 파라미터를 모델 오브젝트 등으로 변환하는 경우이다.
@ModelAttribute
뿐만 아니라 @RequestParam
, @PathVariable
등도 해당된다.
근데 잘 생각해보면, 프로퍼티 바인딩이 일반 primitive 타입
이 아닌 경우에도 가능했던 적이 있었다.
루트 웹 애플리케이션 컨텍스트에서 dataSource
빈을 설정할 때다.
1 | <bean id="dataSource" class="org.springframework..SimpleDriverDataSource"> |
보다시피 value
에 문자열로 클래스명을 전달하고 있다.
그런데 driverClass
프로퍼티는 String
타입이 아닌 Class
타입이다. 하지만 잘 바인딩 된다.
이는 스프링이 제공하는 프로퍼티 바인딩 기능을 사용했기 때문이다.
스프링은 프로퍼티 바인딩을 위해 2가지 API를 제공한다.
스프링이 기본적으로 제공하는 바인딩용 타입 변환 API이다.
PropertyEditor
는 스프링 API가 아니라 자바빈 표준에 정의된 API이다.
GUI 환경에서 비주얼 컴포넌트를 만들 때 사용하도록 설계되었고, 기본적인 기능은 문자열과 자바빈 프로퍼티 사이의 타입 변환이다.
스프링은 이PropertyEditor
를문자열-오브젝트
상호변환이 필요한 XML 설정이나 HTTP 파라미터 변환에 유용하게 사용할 수 있다고 판단하여 이를 일찍부터 사용해왔다.
스프링은 20여가지 정도의 PropertyEditor
를 만들어 디폴트로 제공하고 있다.
아래의 링크에서 확인할 수 있다.
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/propertyeditors/package-summary.html
이 디폴트 PropertyEditor
들은 바인딩 과정에서 파라미터 타입에 맞게 자동으로 선정되어 사용된다.
디폴트 프로퍼티 데이터에 등록되지 않은 타입을 파라미터로 사용하고 싶을 경우, 직접 PropertyEditor
를 만들어 적용할 수 있다.
아래와 같은 enum
이 하나 있고,
1 | public enum Level { |
아래와 같이 컨트롤러를 등록하고 /user?level=1
과 같이 호출하면 자동으로 Level enum
으로 변환해서 받고 싶다고 하자.
(level
파라미터가 Integer
이지만 변환이 간단하므로 문제될 것 없다)
1 | @Controller |
현재는 당연히 변환이 불가능하므로 오류가 발생한다.
Level
타입에 대한 PropertyEditor
를 만들어야 한다.
아래는 프로퍼티 에디터가 변환할 때의 동작 방식이다.
setValue()
, getValue()
는 그냥 getter
,setter
이기 때문에 손댈 것 없고,
실제로 우리가 구현해야 할 메서드는 setAsText()
와 getAsText()
이다.
현재 우리한테 필요한 부분은 문자열 -> 오브젝트
의 과정이므로 setAsText()
메서드를 구현해서 Level enum
에 대한 PropertyEditor
를 만들어보겠다.
1 | public class LevelPropertyEditor extends PropertyEditorSupport{ |
이제 이 PropertyEditor
를 userSearch
메서드에서 사용할 수 있게 등록해줘야 한다.
PropertyEditor
를 추가하기 전에 먼저 컨트롤러에서 메서드 바인딩이 일어나는 순서를 알아보자.
AnnotationMethodHandlerAdapter
는 @RequestParam
, @PathVariable
, @ModelAttribute
와 같이 HTTP 요청을 변수에 바인딩하는 애노테이션을 만나면 먼저 WebDataBinder
라는 것을 만든다.
WebDataBinder
는 여러가지 기능을 포함하는데, 여기에 HTTP 요청 문자열을 파라미터로 변환하는 기능도 포함되어 있다.
즉, 우리가 만든 PropertyEditor
를 사용하려면 이 WebDataBinder
에 직접 등록해줘야 한다.
근데 WebDataBinder
의 변환 과정이 외부로 노출되지 않으므로, 직접 등록해 줄 방법은 없다.
그래서 스프링이 제공하는 WebDataBinder
초기화 메서드를 사용해야 한다.
@InitBinder
애노테이션이 부여되고, WebDataBinder
를 인자로 받는 메서드를 하나 생성하자.1 | @Controller |
그리고 WebDataBinder
의 registerCustomEditor
메서드에 PropertyEditor
를 적용할 타입과 PropertyEditor
인스턴스를 전달해주면 된다.
이후 다시 /user?level=1
을 호출해보면 level
변수에 Level.BASIC
오브젝트가 들어가있는 것을 확인할 수 있다.
WebDataBinder
대신WebRequest
를 받을 수도 있다!
initBinder
메서드는 클래스내의 모든 메서드에 대해 파라미터를 바인딩하기 전에 자동으로 호출된다.
바인딩 적용 대상은 @RequestParam
, @PathVariable
, @CookieValue
, @RequestHeader
, @ModelAttribute
의 프로퍼티 이다.
기본적으로 PropertyEditor
는 지정한 타입과 일치하면 항상 적용된다.
여기에 프로퍼티 이름을 추가 조건으로 주고, 프로퍼티 이름까지 일치해야만 적용되게 할 수 있다.
이러한 타입의 PropertyEditor
는 이미 PropertyEditor
가 존재할 경우 사용한다.
WebDataBinder
는 바인딩 시 커스텀 PropertyEditor
가 있을 경우 이를 선적용하고, 없을 경우 디폴트 PropertyEditor
를 적용하기 때문이다.
아래는 적절한 예시이다.
1 | public class MinMaxPropertyEditor extends PropertyEditorSupport{ |
이렇게 해두면 추가하는 유저의 age
값은 1~50
까지로 제한된다.
PropertyEditor
를 등록할 때 프로퍼티 이름으로 age
를 지정했기 때문에 id에는 적용되지 않는다.
참고로 이 방식은 프로퍼티 이름이 필요하므로 @RequestParam
같은 단일 파라미터 바인딩에는 적용되지 않는다.
@InitBinder
방식은 범위가 컨트롤러 하나로만 제한되므로 다른 컨트롤러에서 사용하려면 또 다시 등록해줘야 한다.PropertyEditor
가 모든 곳에 적용해도 될 만큼 필요한 PropertyEditor
라면 등록하는 방법을 달리하여 모든 컨트롤러에 적용해줄 수 있다.WebBindingInitializer
인터페이스를 구현한 클래스를 작성한다.1 | public class MyWebBindingInitializer implements WebBindingInitializer{ |
이제 이 클래스를 빈으로 등록하고 AnnotationMethodHandlerAdapter
의 webBindingInitializer
프로퍼티에 DI
해주면 전체적으로 적용된다.
1 | <bean class="org.springframework..AnnotationMethodHandlerAdapter"> |
앞서 작성했던 PropertyEditor
등록 코드들을 보면, 매번 new 키워드
로 PropertyEditor
를 생성하고 있다.
이 부분이 뭔가 부담스럽게 생각되어 PropertyEditor
를 빈으로 등록하는 방식을 생각할 수 있는데, 이는 위험한 상황을 초래한다.
위의 PropertyEditor
동작방식을 다시 살펴보면, 변환과정에서 항상 set -> get
의 순서로 2개의 메서드를 사용하고 있음을 볼 수 있다.
이 말인 즉, PropertyEditor
는 짧은 시간이나마 상태를 가진다는 것을 의미한다.
상태를 가지는 오브젝트는 절대 빈으로 등록되서는 안된다.
매번 new 키워드
로 생성되는 부분이 부담스러워 보일 수 있으나, 실상 PropertyEditor
는 워낙 간단한 클래스라 자주 생성되도 별로 문제가 되지 않는다.
그러므로 싱글톤으로 PropertyEditor
를 생성하는 실수를 하지 않도록 주의해야 한다.
근데 개발을 하다보면, PropertyEditor
에서 다른 빈을 DI
받아야 할 경우가 가끔 생긴다.
예를 들면 아래와 같이 변환할 프로퍼티가 하나의 도메인 오브젝트에 대응하는 경우이다.
1 | public class User{ |
이런 경우, 일반적인 방법으로는 변환할 수 없다. 요청 파라미터는 평범한 문자열이기 때문이다.
이 상황을 해결할 수 있는 방법은 2가지가 있다.
Code
오브젝트로 변환하되, 완벽하지 않은 오브젝트로 변환하는 방법이다.1 | public class CodePropertyEditor extends PropertyEditorSupport{ |
이런식으로 전달받은 id
값만 채운 불완전한 Code
오브젝트를 돌려주는 것이다.
이런 방식을 모조 PropertyEditor
라고 부른다.
하지만 이 방식은 조금 위험하다. 다른 프로퍼티들의 값이 모두 null
인 불완전한 오브젝트로 변환해주기 때문이다.
이런 오브젝트는 업데이트가 발생하면 심각한 문제를 초래할 수 있으나,
사실상 이러한 코드성 도메인 오브젝트는 다른 테이블에서 참조하는 용도로만 사용하는 것이 대부분이다.
그래서 이런 부분만 유의해주면 매우 유용하게 활용할 수 있다.
PropertyEditor
를 프로토타입 빈으로 등록하고, 서비스나 DAO
객체를 DI
받아 Code
오브젝트를 조회해오는 방법이다.1 | @Component |
이 방식의 장점은 항상 완전한 도메인 오브젝트를 리턴해주므로, 앞서 제기했던 위험이 없어진다.
단점으로는 매번 DB에서 조회를 해야하므로 성능에 조금 부담을 주는 단점이 있다.
하지만 JPA와 같이 엔티티 단위의 캐싱 기법이 발달한 기술을 사용할 경우, DB에서 조회하는 대신 메모리에서 바로 읽어올 수 있으므로 DB 부하에 대한 걱정은 하지 않아도 된다.
PropertyEditor
는 근본적인 단점이 있다.
상태를 가지고 있으므로 싱글톤으로 등록할 수 없고, 항상 새로운 오브젝트를 만들어야 한다는 점이다.
스프링 3.0이후로 이러한 PropertyEditor
의 단점을 보완해주는 Converter
라는 타입 변환 API가 등장하였다.
Converter
는 PropertyEditor
와 달리 변환과정에서 메서드가 한번만 호출된다.
즉, 상태를 가지지 않는다는 뜻이고, 싱글톤으로 등록할 수 있다는 뜻이다!
아래는 Converter
인터페이스이다.
1 | public interface Converter<S, T>{ |
양방향 변환을 지원하던 PropertyEditor
와는 달리, 단방향 변환만을 지원한다.
(양방향을 원하면 그냥 반대방향의 Converter
를 하나 더 만들면 된다.)
게다가 한쪽 타입이 무조건 String
으로 고정되는 불편함 없이 직접 지정 가능하다.
아래는 전달받은 파라미터를 Level
타입으로 변환해주는 Converter
이다.
1 | public class LevelConverter implements Convert<Integer, Level>{ |
타입을 바로 Integer
로 지정함으로써 지저분한 타입변환 코드를 제거할 수 있다.
PropertyEditor
처럼 직접 등록할 수 없고, ConversionService
타입의 오브젝트를 통해서 WebDataBinder
에 등록해야 한다.
ConversionService
타입의 오브젝트를 빈으로 등록하고 이를 DI
받아 WebDataBinder
에 등록하는 방식이므로 PropertyEditor
에 비해 부담이 적다.
ConversionService
를 등록하는 방법은 2가지가 있다.
첫째로 직접 클래스를 만들고 GenericConversionService
를 상속받은 뒤, addConverter()
메서드로 Converter
들을 등록하는 방식이다. 이후 빈으로 등록한다.
둘째는 추가할 Converter
들을 빈으로 등록해두고 ConversionServiceFactoryBean
을 이용해서 Converter
들이 추가된 GenericConversionService
를 빈으로 등록하는 방식이다.
직접 클래스를 만들지 않고 설정만으로 가능하므로 좀 더 편리하다.
아래는 두번째 방법이다.
1 | <bean class="org.springframework..ConversionServiceFactoryBean"> |
그리고 컨트롤러에서 아래와 같이 해주면 된다.
1 | @Controller |
매번 개별적으로 등록해줘야하는 PropertyEditor
와는 달리 하나의 ConversionService
에 Converter
들을 일괄적으로 지정할 수 있어 매우 편리하다.
때에 따라서는 여러개의 ConversionService
를 만들어놓고 사용하기도 한다.
하지만 WebDataBinder
는 하나의 ConversionService
타입 오브젝트만 허용한다는 점은 알고있어야 한다.
Converter
도 PropertyEditor
처럼 WebBindingInitializer
를 이용해 일괄등록 할 수 있다.
하지만 ConversionService
를 등록할 떄는 ConfigurableWebBindingInitializer
를 이용하는 것이 더 편리하다.
1 | <bean class="org.springframework..ConversionServiceFactoryBean"> |
이게 전부 ConversionService
를 싱글톤 빈으로 등록할 수 있기에 생겨난 방법들이다.
만약 spring 설정에서
<mvc:annotation-driven />
를 사용했을 경우,
위의 두 방법처럼ConversionService
를 등록하는 것이 불가능하다.
이럴경우<mvc:annotation-driven conversion-service="conversionService"/>
처럼 엘리먼트를 이용해서 등록해줘야 하며, 이렇게 등록할 경우 모든 클래스에 자동으로 적용된다.
위의 두 가지 외에 Formatter
라는 타입 변환 API가 하나 더 있다.
근데 이는 스프링에서 기본으로 제공하는 API가 아니라서, 절차가 조금 까다롭다.
일단 Formatter
인터페이스는 아래와 같다.
1 | // Formatter interface |
이 인터페이스를 구현해서 Formatter
를 만들면 된다. 보다시피 Locale
을 파라미터로 받을 수 있어 컨트롤러에 사용하기 좀 더 특화되었다고 할 수 있다.
근데… Formatter
는 스프링 기본 API가 아니라서 GenericConversionService
에 직접 등록할 수 없다.
Formatter
를 GenericConverter
로 포장해서 등록해주는 FormattingConversionService
를 통해서만 등록될 수 있다.
그리고 Formatter
를 본격적으로 사용하려면 이게 끝이 아니라
애노테이션을 연결시켜야 하므로 AnnotationFormatterFactory
도 사용해야 한다.
이래서 굳이 Locale
이 타입 변환에 필요한 경우가 아니라면 Converter
를 사용하는 편이 낫다.
당장은 FormattingConversionServiceFactoryBean
을 통해 FormattingConversionService
를 등록하고, 거기서 기본으로 등록되는 Formatter
만 사용해도 유용하다.
1 | <bean class="org.springframework..FormattingConversionServiceFactoryBean" /> |
이렇게 빈으로 등록하고 위와 같은 방식으로 conversionService
를 주입해주면 된다.
이 또한 만약 spring 설정에서
<mvc:annotation-driven />
를 사용했을 경우 사용이 불가능하다.
참고로FormattingConversionServiceFactoryBean
은<mvc:annotation-driven />
사용 시 디폴트로 등록해주는ConversionService
라 위처럼conversion-service
엘리먼트를 이용해 따로 등록해 줄 필요없다.
NumberFormatter
, CurrencyFormatter
, PercentFormatter
와 연결되어 있다.style
과 pattern
을 줄 수 있다.style
은 Number
, Currency
, Percent
세 가지를 설정할 수 있고, 각각 위의 Formatter
와 연결된다.style
에 없는 패턴을 사용하고 싶을 경우 pattern
엘리먼트를 통해 직접 지정할 수 있다.pattern
엘리먼트를 사용한 예제이다.1 | class Product{ |
request 인자로 price=$100,000
과 같이 넘겨줘도 price
프로퍼티에서 변환해서 받을 수 있고, 뷰로 내려줄 때에 $100,000
의 형태로 내려줄 수 있다.
Joda Time
을 이용하는 애노테이션 기반 포멧터이다.DateTimeFormatter
와 연결되어 있다.style
과 pattern
을 줄 수 있다.S
(short), M
(medium), L
(long), F
(full) 4개의 문자를 날짜와 시간에 대해 1글자씩 사용해 스타일을 지정한다.1 | @DateTimeFormat(style="FS") |
이는 yyyy'년' M'월' d'일' EEEE a h:mm
포멧으로 매핑된다. 물론 각각의 지역정보에 따라 다르게 출력된다.
style
에서 지정한 패턴이 마음에 들지 않는 경우, pattern
엘리먼트를 통해 직접 지정 가능하다.
1 | @DateTimeFormat(pattern="yyyy/MM/dd") |
위의 3가지 방법은 각기 장단점이 있기 때문에 하나만 골라 사용하는 것은 바람직하지 않다.
아래는 어떤 경우에 어떤 바인딩 기술을 활용하는 것이 좋은지에 대한 몇 가지 시나리오이다.
사용자 정의 타입 바인딩을 위한 일괄 적용 : Converter
앞의 Level enum
처럼 애플리케이션에서 정의한 타입이면서 모델에서 자주 활용되는 타입이라면 Converter
로 만들고 ConversionService
로 묶어서 일괄 적용하는 것이 편리하다.
메타정보를 활용하는 조건부 바인딩 : ConditionalGenericConverter
바인딩이 특정 조건(필드, 메서드 파라미터, 애노테이션 등)에 따라 다르게 동작할 때에는 ConditionalGenericConverter
를 이용해야 한다. 구현이 까다롭다.
애노테이션을 통한 바인딩 : AnnotationFormatterFactory
, Formatter
애노테이션을 통해 바인딩 하고 싶을 경우 사용하면 좋다.
특정 필드에만 바인딩 : PropertyEditor
특정 모델의 특정 필드에 제한해서 바인딩을 적용해야 할 경우 PropertyEditor
를 사용하는 것이 편리하다. 필드 이름을 메서드 파라미터로 전달할 수 있기 때문이다.
이렇듯 여러 바인딩 기술들을 등록하다 보면 서로 중복되는 부분이 발생할 것이다.
이럴 경우 우선순위에 의해 바인딩이 적용된다.
Custom PropertyEditor > ConversionService > Default PropertyEditor
중복 시 위의 순서로 바인딩된다.
그리고 WebBindingInitializer
를 통해 등록한 공통 바인딩은 @InitBinder
보다 우선순위가 뒤쳐진다.
WebDatBinder
에는 PropertyEditor
, ConversionService
등록 외에도 여러 유용한 바인딩 옵션들이 있다.
@ModelAttribute
를 사용할 경우 근본적인 보안 문제가 하나 있다.@SessionAttributes
를 사용해 변경할 필드만 폼에 표출했다고 하더라도 사용자가 임의로 폼을 조작하여 전달하는 값에 대해서는 변경을 막지 못한다는 점이다.level
이라는 필드를 폼에 추가하여 전송할 경우 실제 값이 바뀌는 일이 생길 수도 있다는 것이다.여기에 사용되는 것이 위의 두 속성이다.
allowedFields
에는 바인딩을 허용할 필드 목록을 넣을 수 있고, disallowedFields
에는 바인딩을 금지할 필드 목록을 넣을 수 있다.
1 | @InitBinder |
이러면 위의 지정한 필드명 외에 다른 필드는 아무리 HTTP 요청으로 보내봐야 바인딩 되지 않는다.
게다가 *level*
처럼 와일드카드도 사용할 수 있다.
requiredFields
필수 파라미터를 지정할 수 있다.
@ModelAttribute
의 특성 상 파라미터가 들어오지 않았다고 바로 에러를 발생 시키지 않고 BindingResult
에 검증 결과를 저장한다.
하지만 setRequiredFiedls()
메서드로 필수 파라미터를 지정해 줄 경우, 파라미터가 들어오지 않으면 바로 에러가 발생한다.
fieldMarkerPrefix
input checkbox
는 조금 특별한 성질이 있다.
1 | <input type="checkbox" name="type" value="on" /> |
폼에 이와 같은 체크박스가 있다고 했을 때, 이를 체크하고 전달하면 type=on
의 형태로 데이터가 전달되지만, 체크하지 않고 전달하면 아예 값을 전달하지 않는다는 점이다.
즉 수정폼에서 기존에 체크되어있던 체크박스를 해제하고 전달할 경우 아무런 값도 전달되지 않기 때문에 사용자는 값을 변경할 수 없는 문제가 발생하게 되는 것이다.
이럴 때 필드마커
라는 것을 이용해 해결 할 수 있는데, 아래와 같다.
1 | <input type="checkbox" name="type" /> |
_type
의 앞에 붙은 _
를 필드마커
라고 하는데, 스프링은 이런 필드마커가 있는 필드를 발견할 경우, 필드마커를 제외한 이름의 필드가 폼에 존재한다고 생각한다.
즉, 체크박스를 선택하지 않아 type
파라미터가 전달되지 않았지만, _type
필드가 전달 되었으므로 스프링은 type
필드가 폼에 있다고 판단하는 것이다.
그리고 이처럼 _type
은 전달되고 type
은 전달되지 않았을 경우, 체크박스를 해제했기 때문이라 생각하고 해당 프로퍼티 값을 리셋해준다.
리셋 방식은 boolean 타입이면 false, 배열타입이면 빈 배열, 그 외라면 null을 넣어주는 것이다.
WebDataBinder
의 setFieldMarkerPrifix()
메서드는 이 필드마커를 변경해주는 메서드이다. 기본값은 _
이다.
필드 디폴트
는 히든 필드를 이용해 체크박스의 디폴트 값을 지정하는데 사용한다.1 | <input type="checkbox" name="type" value="A"/> |
!type
히든 필드를 지정해서 type
필드의 기본값을 지정해줬다.
이럴 경우 체크박스를 선택하지 않아 type
필드가 전달되지 않을 경우, 디폴트 값인 Z가 전달된다.
모델 프로퍼티 값이 단순값이 아닐 경우 유용하게 사용할 수 있다.
이 또한 setFieldDefaultPrefix()
메서드를 이용해 접두어를 변경해 줄 수 있다. 기본값은 !
이다.
@ModelAttribute
의 바인딩 작업이 실패로 끝나는 경우는 2가지가 있다.
첫째로 @ModelAttribute
가 기본적으로 실행하는 타입 변환에서 오류가 발생했을 경우이고,
둘째로 검증기(validator)
를 통과하지 못했을 경우이다. 이는 사용자가 직접 정의하는 부분이다.
사실상 폼의 서브밋을 처리하는 컨트롤러 메서드에서는 검증기를 이용한 검증 작업은 필수이다.
검증 결과에 따라 다음 스텝으로 넘어가든, 다시 폼을 띄워 수정을 요구하든 해야한다.
이 과정에서 쓰이는 API인 Validator
, BindingResult
, Errors
에 대해 알아보자.
오브젝트 검증기를 정의할 수 있는 API이다. @ModelAttribute
바인딩 때 주로 사용된다.
아래는 Validator
인터페이스이다.
1 | public interface Validator{ |
supports()
는 이 검증기가 검증할 수 있는 타입인지 확인하는 메서드이고,
이를 통과할 경우 validate()
를 통해 검증이 진행된다.
validate()
의 검증과정에서 아무 문제가 없으면 메서드를 정상 종료하면 되고,
문제가 있을 시 Errors
인터페이스에 오류정보를 등록해주면 된다.
이후 이 오류정보를 통해 컨트롤러에서 적절한 작업을 해주면 되는 것이다.
자바스크립트로 입력값을 검증했을 경우 서버에서 검증작업을 생략해도 될까?
안된다. 서버의 검증작업을 생략하면 매우 위험해진다.
브라우저에서 자바스크립트가 동작하지 않게 할수도 있고, 강제로 폼을 조작할수도 있고, Burp suite 같은 것을 사용하여 전달되는 데이터를 변경할 수도 있다.
그러므로 서버 검증작업은 필수로 있어야 한다.
아래는 Validator
구현의 예시이다.
1 | public class UserValidator implements Validator{ |
주석에도 써놓았지만 오류 정보를 등록하는 방법이 다양하다.
rejectValue()
에 사용된 name
은 필드 이름이며, name.required
는 에러 코드를 정의한 것이다.
(이 에러코드는 messageSource
와 함께 사용될 수 있다. 사용 방법 보기)
age
필드를 검증할때, 보다시피 에러코드에 파라미터를 전달할수도 있으며 디폴트 메세지도 전달할 수 있다.
제일 아랫부분처럼 2가지 이상의 필드에 대해 검증하는 경우, 필드명을 생략 가능하다.
ValidationUtils
같은 유틸리티 클래스도 제공되니 잘 활용하면 좋다.
Validator
는 싱글톤 빈으로 등록 가능하기 때문에 서비스 로직을 이용하여 검증작업을 진행할 수도 있다. 대표적인 것이 아이디 중복 검사이다.
근데 사실 이 정도 검증까지 가면 좀 모호해지는게 있는데, 검증이 수행되는 계층이다.
검증 작업을 컨트롤러 로직이라고 보는 개발자도 있는 반면, 대부분이 서비스 계층과 연관이 있으니 서비스 계층의 로직이라고 보는 개발자도 있다.
이는 개인이 잘 판단하면 될 문제인 것 같다.
중요한 것은 어느 곳에서 사용하든, 위와 같이 검증로직은 따로 분리되어 있는 것이 좋다.
Validator
는 빈으로 등록 가능하니 이를 컨트롤러에서 DI
받은 뒤, 각 컨트롤러 메서드에서 validate()
를 직접 호출해서 검증을 진행하는 방식이다.supports()
는 생략 가능하다)1 | @Controller |
@Valid
를 이용한 자동 검증JSR-303의 @javax.validation.Valid
애노테이션을 사용하는 방법이다.validate()
를 호출하여 검증하던 방식과 달리, 바인딩 과정에서 자동으로 검증이 진행되도록 할 수 있다.1 | @Controller |
WebDataBinder
에는 보다시피 Validator
타입의 검증용 오브젝트도 등록할 수 있다.
그리고 아래 @ModelAttribute
를 사용하는 부분에 추가로 @Valid
애노테이션을 사용해주면 자동으로 검증작업이 수행된다.
개인적으로 위의 방식보다 훨씬 나아 보인다 ㅋㅋ
참고로 @InitBinder
말고 WebBindingInitializer
를 이용해 모든 컨트롤러에 일괄 적용할 수도 있다.
Validator
가 싱글톤 빈으로 등록되기에 서비스 계층에서도 얼마든지 DI 받아 사용할 수 있다.BindingResult
타입 오브젝트를 직접 만들어서 validate()
에 전달해야 하는데, 이때는 BeanPropertyBindingResult
를 사용하는 것이 적당하다.서비스 계층을 활용하는
Validator
Validator
는 싱글톤 빈으로 등록될 수 있으므로 다른 빈을 DI받아 사용할 수 있다.
앞서 예시로 들었던 ID 중복 검사처럼,Validator
내에서 서비스 계층 빈을 사용하여 검증할 수도 있다.
게다가 이 경우 결과를BindingResult
에 담으면 되므로 서비스 계층에서 번거롭게 예외를 던지던 방식을 제거할 수 있다.
대신 이 방식을 사용하면 컨트롤러에서 서비스 계층을 두번 호출한다는 단점이 있다.
하지만 전체적으로 코드가 깔끔해지고 역할 분담이 확실해지는 장점이 있다.
@Valid
를 포함하고 있는 JSR-303
의 빈 검증 방식도 스프링에서 사용할 수 있다.
1 | public class User{ |
이런식으로 모델에 특정 애노테이션만 작성해주면 된다. Validator
를 직접 구현해서 검증기를 만들필요 없이 간단하게 검증작업을 진행할 수 있다.
이 검증 방식을 사용하려면 LocalValidatorFactoryBean
을 사용해야 한다.
LocalValidatorFactoryBean
이 생성하는 클래스 타입은 Validator
이므로 이를 빈으로 등록한 뒤 DI
받아 사용하면 된다.
(컨트롤러에서 직접 생성해도 되고, WebDataBinder
에 등록해도 된다)
앞서 Validator
에서 errors.rejectValue("name", "name.required")
와 같이 에러코드를 지정하던 작업이 기억날 것이다.
이 정보는 보통 컨트롤러에 의해 폼을 다시 띄울 때 활용된다.
스프링은 등록된 에러 코드를 아래의 같은 파일에서 찾아와 에러 메세지로 활용한다.
1 | name.required=이름은 필수로 입력하셔야 합니다. |
근데 이렇게 지정한 에러코드로 바로 메세지를 찾는것은 아니고, 스프링의 MessageCodeResolver
라는 것을 거쳐 에러코드를 확장하는 작업을 한번 거친다.
(스프링의 디폴트 MessageCodeResolver
는 DefaultMessageCodeResolver
이다.)
이 리졸버를 거치게 되면 우리가 등록한 에러코드 name.required
는 아래와 같이 4가지 에러코드로 확장된다.
이 4가지 에러코드는 위에서부터 우선순위를 가진다.
즉 메세지 파일이 아래와 같다면,
1 | name.required.user.name=무언가 잘못된 메세지 |
우리는 에러코드를 분명 name.required
라고 지정했지만 계속해서 무언가 잘못된 메세지
가 출력될 것이다.
그러므로 에러코드 지정 시 어떤 에러코드로 확장되는지 정확히 알고 있어야 한다.
아니면 위처럼 의도하지 않은 상황이 발생할 수 있기 떄문이다.
(개인적으로 좀 혼란스러운 방식이라고 생각한다…)
위는 한가지 예시였을 뿐이고, 검증방식(rejectValue
, reject
, 타입 오류 등, JSR-303
)에 따라 에러코드가 확장되는 룰이 다르니 사용할 떄 주의해야 한다.
위에서 확장된 에러코드는 마지막으로 MessageSourceResolver
라는 것을 거쳐 실제 메세지로 생성된다. 이 때 사용하는 것이 이 MessageSource
이다.
이는 디폴트로 등록되지 않으니 스프링의 빈으로 직접 등록해줘야 한다.
MessageSource
는 2가지 종류가 있는데 보통 ResourceBundlerMessageSource
를 사용한다.
이는 일정시간마다 메세지 파일 변경 여부를 확인해서 메세지를 갱신해주므로 서버가 구동중인 상황에도 메세지를 변경해줄 수 있다.
1 | <bean id="messageSource" class="org.springframework...ResourceBundleMessageSource" /> |
속성값으로 메세지 파일을 지정해주지 않을 경우 디폴트로 messages.properties
파일이 사용된다.
MessageSource
는 아래의 4가지 정보를 활용해 최종 메세지를 생성한다.
코드
메세지 파일은 키=벨류
의 형태로 등록되어 있기 때문에, 메세지를 찾을 키 값은 필수이다.
앞서 우리가 Errors
에 등록했던 에러 코드가 이 키 값인 것이다.
메세지 파라미터 배열
앞서 에러코드를 등록하며 Object[]
타입의 파라미터를 넘겨줬던 것을 기억할 것이다.
해당 파라미터는 메세지를 생성하는데 사용될 수 있다.
메세지 파일은 아래와 같이 작성된다.
1 | field.min={0}보다 작은 값을 사용할 수 없습니다. |
파라미터는 1개 이상 올 수 있기때문에 Object
배열을 사용한다.
디폴트 메세지
코드에 맞는 메세지를 찾지 못하였을때 디폴트 메세지를 지정해줄 수 있다.
에러코드를 충실히 적용했다면 이 부분은 생략하거나 null
로 주면 된다.
참고로 코드에 해당하는 메세지도 없고, 디폴트도 없을 경우 예외가 발생하니 주의해야 한다.
지역정보
LocaleResolver
에 의해 결정된 현재의 지역정보를 사용할 수 있다.
지역정보에 따라 다른 프로퍼티 파일을 사용 가능하다.
만약 messages.properties
파일을 사용했다면 Locale
이 ENGLISH
일 경우 messages_en.properties
파일이 사용된다.
이를 통해 다국어 서비스를 적용할 수 있다.
모델은 MVC 아키텍쳐에서 정보를 담당하는 컴포넌트이다.
요청 정보를 담기도 하고, 비즈니스 로직에 사용되기도 하고, 뷰에 출력되기도 한다.
또한 이 모델은 아주 여러곳을 거쳐가며 만들어지고, 변형된다.
그러므로 모델의 사이클에 대한 지식은 스프링 MVC를 사용할 떄 가장 중요하다고 할 수 있다.
HTTP 요청에서 컨트롤러 메서드까지
왼쪽에서 오른쪽으로 보면 된다.
컨트롤러 메서드에서 뷰까지
오른쪽에서 왼쪽으로 보면 된다.
모델의 생성, 변형, 사용이 한눈에 볼 수 있게 잘 표시되어 있다.
기본적으로 하드는 데이터를 읽을때 원판 플래터를 회전시키며 데이터를 찾는다.
순차 IO란 시작위치에 간 뒤 쭉 읽어서 데이터를 찾는 것을 말하고,
랜덤 IO란 여러 위치를 탐색해서 최종적으로 데이터를 찾는 것을 말한다.
인덱스 레인지 스캔의 경우 랜덤 IO, 테이블 풀 스캔의 경우 순차 IO를 사용한다.
보다시피 당연히 랜덤 IO가 순차 IO 보다 성능이 떨어진다.
하지만 여기서 SSD를 사용하면 얘기가 달라진다.
SSD는 HDD와 달리 플래터에 데이터를 기록하지 않고, 플래시 메모리라는 것에 데이터를 저장한다.
이는 원판을 기계적으로 회전시킬 필요가 없으므로 데이터를 매우 빠르게 찾을 수 있다.
그러므로 DBMS용 스토리지에는 SSD가 최적이라고 볼 수 있다.
책을 데이터에 비유한다면, 인덱스는 색인에 비유할 수 있다.
색인에서 키워드와 페이지 번호를 쌍으로 연결해놓았듯이, 인덱스 또한 인덱스 컬럼 값들과 레코드 주소를 키,벨류 형태의 쌍으로 저장해놓은 것을 말한다.
즉 데이터를 검색 할 때 인덱스를 통하는 방식으로 원하는 레코드에 빠르게 접근 할 수 있다.
자료구조로 비교해봤을 때
인덱스를 역할로 분류했을 때,
인덱스 저장 방식으로 분류했을 때,
인덱스를 중복 여부로 구분했을 때,
가장 일반적으로 사용되는 인덱스 저장 자료구조.
컬럼의 값을 아무런 변형없이 저장한다.
최상단의 루트 노드, 최하단의 리프 노드, 그 둘을 잇는 여러개의 브런치 노드들이 있다.(저장된 데이터가 작을 경우 브런치 노드는 없을 수 있다. 루트랑 리프노드는 항상 존재한다.)
루트 노드들은 각각 자식 노드들의 주소를 가지고, 최하단 리프노드는 저장된 레코드의 주소를 가진다.
테이블에 레코드를 추가하게 되면 키 값을 이용해 들어갈 리프노드의 위치를 찾고, 추가한다.
만약 리프노드에 더 이상 들어갈 공간이 없으면 리프노드를 하나 더 추가하게 되는데, 여기서 상위 리프노드 또한 조정되어야 한다.
이러한 이유 때문에 B-Tree 인덱스에서는 데이터의 추가 작업 비용이 높다.
MyISAM이나 Memory DB의 경우 레코드 추가 -> 인덱스 추가의 작업이 바로바로 이뤄지므로 인덱스에 데이터가 들어가기 전까지 사용자는 결과를 받지 못한다.
이에 비해 InnoDB는 이를 좀 더 유연하게 처리한다.
레코드가 추가되었을 때 리프노드에 들어갈 공간이 남았을 경우 바로 추가하고, 공간이 없을 경우 인서트 버퍼라는 곳에 따로 저장해둔다.
이후 백그라운드 프로세스에서 인덱스를 읽을때나 데이터베이스 서버의 자원이 여유로울 경우 인서트 버퍼 스레드에서 인서트 버퍼를 체크한 뒤 인덱스에 머지한다.
중요한 것은 사용자가 이 작업을 인지하지 않아도 되게끔 투명하게 처리된다는 것이다.
레코드가 삭제되면 그에 해당하는 인덱스에 삭제마크를 표시한다.
이후 저장되는 레코드는 마지막 리프노드 뒤에 붙을수도 있고, 삭제마크된 부분을 재활용하여 인서트 될 수도 있다.
삭제마크를 표시하는 작업도 인서트와 마찬가지로 버퍼를 이용해 지연 처리할 수 있다(MySql 5.5부터)
인덱스 키 값에 따라 인덱스가 들어갈 리프노드의 위치가 정해지므로, 변경은 불가능하고 삭제 -> 추가의 작업으로 진행된다.
작업 방식은 위의 삭제, 추가 방식과 동일하다.
동등연산, 범위연산, Like(검색어%
) 연산에서 인덱스를 사용할 수 있다.
부정연산, Like(%검색어
)에서는 인덱스를 사용할 수 없다.
또한 인덱스 키 값에 변형이 일어난 경우(연산, 형변환)에도 인덱스를 이용한 빠른 검색이 불가능하다.
기본적으로 MySql에서는 데이터의 저장 공간에 페이지라는 최소 단위를 사용한다.
인덱스의 각 노드들도 하나의 페이지로 볼 수 있다.
MySql에서 페이지의 기본 단위는 16KB
이다. 변경하려면 소스를 수정하고 컴파일해야 한다.
인덱스 키 값의 크기가 커지면 자연스럽게 하나의 리프 노드(페이지)에 들어갈 수 있는 데이터의 개수가 작아진다.
만약 인덱스 키 값의 크기가 16바이트이고, 저장된 주소값의 크기가 12바이트 정도라고 하자.
이럴 경우 하나의 리프 노드에 들어갈 수 있는 데이터의 개수는 (16+12)/16KB
해서 585개가 된다.
그리고 만약 인덱스 키 값의 크기가 32바이트라면 (32+12)/16KB
해서 372개가 된다.
이런 상황에서 인덱스를 500개 읽어야 한다고 가정해보면, 리프노드 하나만 읽어도 되었을 것을 리프노드를 2개에 걸쳐 읽어야 하는 상황이 발생한다.
이로 인해 추가적인 I/O가 발생하게 되고, 속도가 느려지게 된다.
만약 2억개의 인덱스를 저장해야 하는 상황이 있다고 가정해보자.
인덱스 키 값의 크기가 16바이트일 경우 한 리프당 585개의 데이터가 저장 가능하기 때문에
585^3 = 200,201,625 로 3depth로 2억개의 레코드에 대한 인덱스를 저장 가능하다.
하지만 만약 32바이트일 경우 한 리프당 372개만 저장 가능하기 때문에
372^3 = 51,478,848 밖에 안되므로 3depth로 모든 인덱스를 저장하지 못하고, depth가 깊어지는 상황이 발생한다.
당연하게도 depth가 깊어지면 그만큼 I/O가 늘어나게 되고, 속도가 느려지게 된다.
실제로 depth가 아무리 깊어져도 4-5depth라고 한다. 인덱스 키 값의 크기를 작게해야 한다는 것을 강조하기 위한 약간 극단적인 예시였다.
인덱스에 값의 그룹이 많을 경우 분포도가 좋다고 하고, 값의 그룹이 작을 경우 분포도가 나쁘다고 한다.
예를 들어 성별
같은 경우 값의 그룹이 남,여 뿐이므로 분포도가 상당히 나쁜 편이다.
아래과 같은 상황이 있다고 가정해보자
1 | -- index : 단일 인덱스(country) |
테이블의 데이터는 10,000개라고 가정하고, country='KOREA' AND city='SEOUL'
인 레코드가 1건 뿐이라고 가정해보겠다.
country가 KOREA인 데이터 1000개를 인덱스로부터 읽어온 뒤, 데이터파일에 랜덤 액세스를 하며 city가 SEOUL인 데이터를 찾는다. 총 999건의 불필요한 검색을 하게 된다.
country가 KOREA인 데이터 10개를 인덱스로부터 읽어온 뒤, 데이터파일에 랜덤 액세스를 하며 city가 SEOUL인 데이터를 찾는다. 총 9건의 불필요한 검색을 하게 된다.
보다시피 1번과 같은 인덱스는 좋지 않다고 할 수 있다.
어쩌피 모든 데이터 상황에 맞출 수 없으므로 불필요한 검색을 0건으로 만드는 것은 거의 불가능하다.
그래도 최대한 2번 인덱스처럼 검색하여 낭비를 최소화 하도록 해야 한다.
일반적으로 인덱스를 이용해 레코드를 읽는 행위가 레코드를 직접 읽는 행위에 비해 3-4배 정도 비용이 크다고 산정한다.
인덱스의 리프 노드까지 가서 레코드의 주소를 찾고 이 주소로 레코드를 읽는 과정에서 랜덤 I/O가 발생하기 때문이다.
인덱스는 각자의 정렬기준으로 정렬되어 있지만 데이터 파일은 그렇지 않기 때문이다.
(인덱스를 통해 3건의 데이터를 찾았을 경우 총 3번의 랜덤 I/O가 발생하는 것이다.)
그래서 테이블 전체 레코드 개수의 20-25%를 넘는 데이터를 인덱스로부터 읽어야 할 경우에 옵티마이저는 그냥 풀 테이블 스캔을 시전한다.
(풀 테이블 스캔의 경우 그냥 순차 I/O로 읽어내리기 때문)
여기서 강제로 인덱스를 타게 해봐야 성능상 별로 효과가 없다.
가장 빠른 스캔 방법이다.
동일 연산자로 하나만 읽으나 범위 연산자로 여러개를 읽으나 모두 인덱스 레인지 스캔으로 분류한다.
루트 노드부터 브랜치 노드를 따라 리프 노드의 데이터(들)를 읽는 방식이다. 이를 탐색한다라고 한다.
인덱스를 처음부터 끝까지 다 읽는 방식이다.
인덱스에 저장된 데이터만으로 모든 것을 처리할 수 있는 경우이거나, 멀티인덱스의 중간 값 부터 조건을 지정하였을 경우 발생한다.
리프 노드의 첫번째 데이터 부터 순차적으로 읽어 내려가며, 하나의 리프노드가 끝났을 경우 해당 리프노드의 링크드리스트를 통해 다음 리프노드로 넘어가 끝까지 읽는 방식이다.
인덱스 풀 스캔은 좋은 방식이 아니다.
말 그대로 루~스하게, 인덱스를 다 읽지않고 듬성듬성 읽는것을 말한다.
중간마다 필요치 않은 인덱스 키 값을 무시하고 다음으로 넘어가는 형태로 처리한다.
예를 들면 아래와 같이 GROUP BY 집합 함수 가운데 MIN, MIX에 대한 최적화를 할 때 사용할 수 있다.
1 | -- index : 복합 인덱스(dept_no, emp_no) |
인덱스는 이미 dept_no, emp_no 의 순서로 정렬되어 있기 때문에 emp_no의 MIN값을 찾고자 할 경우
dept_no 인덱스의 첫번째 컬럼만을 읽으면 된다. 나머지 애들은 굳이 읽을 필요가 없다.
DISTINCT도 루스 인덱스 스캔을 사용한다. 처음 한건만 읽으면 되기 때문이다.
보통 1개의 컬럼으로 인덱스를 생성하기 보단 여러개의 컬럼으로 인덱스를 생성하는 경우가 많다.
다중 컬럼으로 인덱스를 생성할 경우 인덱스의 순서를 신중하게 생각해야 하는데, 이는 인덱스의 정렬이 자신의 앞 인덱스의 정렬에 의존하기 때문이다.
예를 들어 dept_no과 emp_no 컬럼으로 다중 컬럼 인덱스를 생성했다고 가정해보자.
이때 emp_no의 값이 아무리 낮더라도, 짝지어진 dept_no의 값이 높을 경우 해당 데이터는 인덱스의 아래쪽에 쌓이게 된다.
첫번째 컬럼인 dept_no에 의존하기 때문이다.
이 때문에 다중 컬럼 인덱스를 생성할 때에는 순서에 매우 신중해야 한다.
인덱스의 효율(속도)와 연관이 있기 때문.
현재는 어떤지 모르곘으나… MySQL 5.대만 해도 인덱스 생성시 정렬 기준을 주는 것이 불가능했다.
1 | CREATE INDEX idx_test ON test_table(col1 ASC, col2 DESC) |
이렇게 작성해봐야 모두 ASC로 생성된다는 뜻이다.
한쪽 정렬이 적용된 다중 컬럼 인덱스를 가진 테이블에서 컬럼마다 정렬을 지정할 경우, 추가적인 정렬이 일어나기 때문에 절대 빠르게 처리될 수 없다.
이와 같은 상황에서 위와 같이 인덱스를 처리하는게 제일 좋긴하나… 안된다.
그래서 역 값을 줘서 위의 처리가 동작하게 하는 방식을 사용하곤 한다.(col2의 값을 전부 -로 세팅)
MySQL 옵티마이저는 기본적으로 ASC, DESC에 대한 개념이 있다.
ASC로 요청할 때에는 인덱스의 최소값(위)부터 차례로 읽으면 된다는 것을 알고,
DESC로 요청할 때에는 인덱스의 최대값(아래)부터 차례로 읽으면 된다는 것을 알고 있다.
이러한 특성 때문에 우리는 정렬을 공짜로 얻을 수 있다!
인덱스 레인지 스캔이 불가능한 경우
기본적으로 B-Tree는 왼쪽의 데이터에 의존하는 방식이다.
루트 노드부터 해서 여러 depth를 거쳐 리프 노드를 찾는 방식도 결국 왼쪽의 정렬 기준에 의존하는 것이고,
다중 컬럼에서 인덱스의 저장 구조를 보았을 때도 결국 왼쪽의 정렬 기준에 의존하는 것이다.(N번째 컬럼은 N-1번째 컬럼의 정렬기준에 의존한다는 정의)
이런 상황에서 왼쪽의 기준이 불명확할 경우, 인덱스 레인지 스캔이 불가능해진다.
예를 들어 아래와 같은 쿼리가 있다고 하자.
(인덱스 = firstname)
1 | SELECT * FROM employees WHERE first_name LIKE '%mer'; |
왼쪽 값을 기준으로 트리를 형성하는 B-Tree 인덱스인데, 위의 조건은 문자열의 왼쪽값이 정해지지 않았으므로 인덱스를 통한 탐색이 불가능한 쿼리이다.
실제로 돌려보면 테이블 풀 스캔을 한다.
이번에는 다중컬럼 인덱스이다.
(인덱스 = dept_no, emp_no)
1 | SELECT * FROM dept_emp WHERE emp_no >= '11444'; |
이 또한 dept_no을 기준으로 인덱스가 정렬되어 있는데 emp_no 부터 조회했으므로 인덱스를 사용하지 못한다. 루트 노드로 들어갈 수 조차 없기 때문이다.
실제로 돌려보면 테이블 풀 스캔을 한다.
dept_no, emp_no
순서의 인덱스와 emp_no, dept_no
순서의 인덱스가 있고, 아래와 같은 쿼리를 실행한다고 가정해보자.
1 | SELECT * FROM dept_emp WHERE dept_no='d0002' AND emp_no >= '11444'; |
첫번째 인덱스의 경우 dept_no, emp_no의 순서로 정렬되어 인덱스에 저장되어 있으므로 단계적으로 작업의 범위를 줄여나가며 스캔이 가능하다.
하지만 두번째 인덱스의 경우 emp_no, dept_no의 순서로 저장되어 있는데, emp_no에서 동등조건이 아닌 범위 조건을 하고 있다.
이러면 이후 dept_no은 범위 조건으로 검색된 결과에 대해 자신의 조건이 맞는지 안 맞는지의 필터 조건밖에 수행하지 못하게 된다.
즉, 작업의 범위를 줄여나가며 스캔을 하지 못했다.
위의 A 처럼 조건을 더 해갈수록 작업의 범위를 줄여주는 조건을
작업 결정 범위 조건
이라고 하고,
오로지 가져온 데이터에서 해당 조건이 맞는지 안맞는지만을 체크하는 조건을체크 조건
이라고 한다.
1 | 왜 범위 연산이 들어가는 순산부터 작업 범위를 좁히지 못하는 것일까? 생각해봤는데.. |
아래의 인덱스를 보고 어떻게 하면 작업 결정 범위 조건으로 사용할 수 있는지, 어떻게하면 사용하지 못하는지 살펴보자.
1 | CREATE INDEX idx_test ON test_table(col1, col2, col3, ... colN) |
아래와 같이 사용할 경우 작업 결정 범위 조건으로 사용하지 못한다.
인덱스의 첫 컬럼은 무조건 검색되어야 한다. 루트 인덱스로 들어갈 통로이기 때문이다.
아래와 같이 사용했을 경우 작업 결정 범위 조건으로 사용할 수 있다.(i = 2이상 N이하)
위의 두 가지 조건이 성립해야 한다. 동등 조건으로 시작해서 범위조건이나 LIKE(좌측 일치) 조건으로 들어가는 곳 까지가 작업 결정 범위 조건이다.
그 이후로는 모두 체크 조건으로 사용된다.
※ 다중 컬럼 인덱스 사용시 WHERE 조건의 순서는 상관없다.(where절의 실행순서야 옵티마이저에 의해 적절히 잘 파싱된다는걸 말하려는 듯)
1 | SELECT * FROM test_table |
(WHERE 절의 순서를 바꿔도 결과는 동일하다)
인덱스 col1 부터 하나씩 타고 가다가 col3 에서 범위조건을 만남으로써, 이후의 값들은 체크 조건으로 사용되게 된다
col1 ~ col3 = 작업 결정 범위 조건, col4 ~ col5 = 체크 조건
이는 모든 B-Tree에 적용되는 조건이므로 다른 RDBMS에서도 적용할 수 있다.