// 에드센스

디스크 읽기 방식

  • 데이터베이스 성능 튜닝은 디스크 IO를 어떻게 줄이느냐가 관건
  • 데이터베이스에서 병목지점은 항상 디스크 장치
    • 하드디스크
    • SSD → 하드디스크와 비교해서 용량이 적으며 비싸지만 더 사용된다 왜? 랜덤IO가 빨라서

 

순차IO와 랜덤IO

  • 디스크 헤더가 안움직이고 한번에 읽는다 → 순차IO
  • 헤더 이동없이 얼마나 많이 읽을 수 있는가 → 디스크의 성능
  • 순차IO가 랜덤IO보다 항상 성능이 좋다 (약 3배정도)(SSD의 경우도 그렇다)
  • so, 쿼리튜닝 == 랜덤IO를 줄인다

 

인덱스란?

  • DBMS가 데이터베이스 테이블의 모든 데이터를 검색해 원하는 결과를 가져오려면 시간이 오래 걸린다.
  • 그래서 컬럼들의 값과 해당 레코드가 저장된 주소를 키와 값의 쌍으로 삼아 인덱스를 만들어두는 것
  • DBMS의 인덱스는 컬럼의 값을 주어진 순서로 미리 정렬해서 보관한다.
  • 키밸류로 컬럼값과 레코드가 저장된 주소를 미리 저장해두는 자료구조
  • INSERT, UPDATE, DELETE 성능을 포기하고 읽기 속도에 몰빵하는 기능

 

인덱스 알고리즘

  • 비트리 알고리즘
  • 해쉬 인덱스 알고리즘

 

B-Tree 인덱스

  • 칼럼의 원래 값을 변형하지 않고 인덱스 구조체 내에서는 항상 정렬된 상태로 유지한다
  • 데이터베이스에서 인덱스와 실제 데이터가 저장된 데이터는 따로 관리되는데,
  • 인덱스의 리프 노드는 항상 실제 데이터 레코드를 찾아가기 위한 주솟값을 가지고 있다.

 

B-Tree는 왜 빠른가?

  • 어떠한 값에 대해서도 같은 시간에 결과를 얻을 수 있다
  • 리프노드를 선형 탐색한다면 71이라는 값을 찾기위해 O(n)
  • B-tree는 O(logN)

 

B+Tree

  • 리프노드끼리 LinkedList형태로 연결됨
  • 하나의 노드에 더 많은 데이터를 담기에 트리의 높이가 낮아짐
  • 리프노드에 모든 데이터가 있기에 풀스캔시 리프노드의 LinkedList를 한번만 선형탐색하면 된다

 

 

B-Tree 인덱스 사용에 영향을 미치는 요소

 

인덱스 키 값의 크기

  • 인덱스도 페이지 단위로 관리됨
  • 인덱스를 구성하는 키값의 크기가 커지면 디스크로부터 페이지를 읽는 횟수가 늘어난다 → 느려진다
    • 이노디비의 기본 페이지 크기는 16KB
    • 추가

 

비트리의 깊이

  • 깊이도 중요하지만 제어할 수 없다
  • 인덱스 키 값이 커지면 하나의 인덱스 페이지에 더 적은 수의 키 값이 저장되고, 비트리의 깊이가 깊어짐 → 더 많은 디스크 읽기

 

인덱스의 선택도(기수성)

  • 선택도(Selectivity) or 기수성(Cardinality)
  • 모든 인덱스 값 중에 유니크한 값의 수
    • 100개중에 유니크한게 10개라면 기수성은 10
  • 선택도가 높다면 검색 대상이 줄어들기에 빠르다
SELECT * FROM tb_city
WHERE country='KOREA' AND city='SEOUL';
  • 다음과 같은 조건
    • tb_city 테이블에는 1만건의 데이터
    • country 컬럼에만 인덱스
    • country와 city 컬럼은 중복없다
  • country 컬럼의 유니크 값이 10개일 때
    • country=’KOREA’ 라는 조건으로 인덱스를 검색하면 1만 건의 데이터 중 1,000건(10,000 / 10)의 데이터가 일치할 것이라 예상할 수 있다.
    • city=’SEOUL’인 레코드는 1건이므로 1,000건 중 999건이 불필요하게 읽히는 것으로 볼 수 있다.
  • country 컬럼의 유니크 값이 1,000개일 때
    • country=’KOREA’ 라는 조건으로 인덱스를 검색하면 1만 건의 데이터 중 10건(10,000 / 1,000)의 데이터가 일치할 것이라 예상할 수 있다.
    • city=’SEOUL’인 레코드는 1건이므로 10건 중 9건이 불필요하게 읽히는 것으로 볼 수 있다.

 

읽어야하는 레코드의 수

  • 인덱스로 읽어야하는 데이터가 전체 테이블 레코드의 20~25%를 넘어가면 보통 인덱스를 안쓰고 풀스캔
    • 예시로, 100만건중에 50만건을 읽는 쿼리가 있다면 인덱스 사용안하고 풀스캔

 

 

MySQL의 인덱스 사용방법

  • 인덱스 레인지 스캔
  • 인덱스 풀 스캔
  • 루스 인덱스 스캔 (Loose)
  • 인덱스 스킵 스캔

 

인덱스 레인지 스캔

  • 가장 대표적인 방식
  • 검색해야할 인덱스 범위가 결정됐을때 사용
  • 인덱스의 리프노드에서 검색 조건에 일치하는 건들은 데이터 파일에서 레코드를 읽어오는 과정이 필요하다
SELECT * FROM employees WHERE first.name BETWEEN 'Ebbe' AND 'Gad';

  • 두꺼운 화살표가 실제 스캔하는 범위이다
  • 루트노드 → 브랜치노드 → 리프노드를 거쳐 필요한 레코드의 시작점을 찾는다
  • 리프노드에서 Ebbe와 Gad를 찾았다면 그 사이를 스캔한다
  • 그 사이에 대해서는 실제 데이터 파일로 랜덤엑세스를 통해 가져오는 과정이 필요하다
  • 만약 커버링 인덱스가 적용됐다면 이 과정 없음
    • 쿼리를 충족시키는 데 필요한 모든 데이터를 갖고 있는 인덱스를 커버링 인덱스라고한다

커버링 인덱스

  • customer_id에만 인덱스가 있다고 했을때
select *
from temp_ad_offset
where customer_id = 7;

select customer_id
from temp_ad_offset
where customer_id = 7;

Extra 항목은 옵티마이저가 어떻게 동작하는지에 대해 알려주는 힌트 값

  • Using filesort: Orderby처리가 인덱스를 사용하지 못할때
  • Using index: 커버링 인덱스 사용할때
  • Using temporary: 쿼리의 중간결과를 위해 임시테이블을 생성했을때
  • Using index for skip scan: 인덱스 스킵 스캔 최적화를 한 경우

https://zzang9ha.tistory.com/436

 

MySQL EXPLAIN 실행계획 마스터하기(feat. RealMySQL 8.0)

💯 MySQL EXPLAIN 실행계획 마스터하기(feat. RealMySQL 8.0) 실행 계획(EXPLAIN) 이란? 대부분의 DBMS는 많은 데이터를 안전하고, 빠르게 저장 및 관리하는 것이 주목적이다. 이러한 목적을 달성하기 위해 사

zzang9ha.tistory.com

 

 

 

인덱스 풀 스캔

  • 인덱스의 처음부터 끝까지 모두 읽는 방식
  • 인덱스 전체 크기는 테이블의 전체크기보단 작기에 테이블 풀스캔보단 나은 비용
  • (A, B, C) 라는 인덱스가 있다
SELECT * FROM employees WHERE B = 'b' AND C = 'c';
  • 조건절에 사용된 컬럼이 A를 포함하지 않기에 풀스캔 발생

 

루스 인덱스 스캔

  • 듬성듬성 인덱스를 읽는 방식
  • 인덱스 레인지 스캔과 비슷하지만, 중간에 필요하지 않은 인덱스 키 값은 무시한다
  • 일반적으로 groupby나 max(), min() 함수에 대한 최적화로서 옵티마이저가 판단한다

 

인덱스 스킵 스캔

  • MySQL8.0부터 추가된 기능
  • 조건절에 첫번째 인덱스가 없어도 두번째 인덱스만으로 인덱스 사용하게해주는 기능
  • 바로 위 인덱스 풀 스캔의 쿼리가 인덱스를 타게된다
  • 단,
    • where절에 포함되지않은 인덱스 컬럼의 유니크한 개수가 적어야함(A컬럼의 선택도가 낮아야함)
    • 그렇지 않다면 인덱스에서 스캔해야 할 시작지점을 검색하는 작업이 많아지며 오히려 더 느려진다
    • 커버링 인덱스를 사용할 수 있는 경우에만 스킵스캔이 된다.

 

 

멀티밸류 인덱스

  • 다중컬럼 인덱스(복합인덱스)
  • 각 컬럼은 자기 바로 앞 컬럼에 의존하여 정렬돼있음
    • 두번째 컬럼은 첫번째 컬럼에 의존해 정렬돼있다
    • 세번째 컬럼은 두번째 컬럼에 의존해 정렬돼있다
  • 선택도가 높은 컬럼을 앞쪽으로
    • where절에서 자주 사용되는 조건을 앞쪽으로
    • join조건에서 자주 사용되는 조건을 앞쪽으로
    • orderby절에서 자주 사용되는 조건을 앞쪽으로

 

 

인덱스 스캔 방향

  • 인덱스를 생성할 때 설정한 규칙에 따라서 오름차순/내림차순으로 정렬되어 저장된다
  • 오름차순으로 저장되었다고 해서 오름차순으로만 이용할 수 있는건 아니다
    • 오름차순 인덱스를 거꾸로 읽으면 내림차순으로 정렬된 인덱스로도 활용 가능
    • 실행계획에서 결정됨
    • ORDER BY 처리나 MIN() 또는 MAX() 함수 등의 최적화가 필요한 경우 인덱스를 읽는 순서만 변경해서 인덱스 생성 시 지정한 정렬 규칙에 대한 문제점을 해결할 수 있다.

 

내림차순 인덱스는 느리다

  • 왜?
    • 페이지 잠금이 인덱스 정순 스캔에 적합한 구조이므로
    • 페이지 내에서 인덱스 레코드가 단방향으로만 연결된 구조이므로
  • 쿼리에서 자주 사용되는 정렬 순서대로 인덱스를 생성하는 것이 잠금 병목 현상을 줄이는 데 도움됨

 

 

B-tree 인덱스의 가용성과 효율성

  • where조건이나 groupby, orderby절이 어떤 경우에 인덱스를 사용하는지 알아야함

 

비교 조건의 종류와 효율성

SELECT * FROM dept_emp
WHERE dept_no= 'd002' AND emp_no >= 10114 ;
  • 1번: 인덱스가 (dept_no, emp_no)인 경우
  • 2번: 인덱스가 (emp_no, dept_no)인 경우

1번 케이스

  • dept_no가 ‘d002’이고 emp_no가 10114보다 큰 레코드를 찾는다.
  • 이후에는 dept_no가 ‘d002’가 아닐 때까지 인덱스를 쭉 읽기만 하면 된다.

2번 케이스

  • emp_no가 10114 보다 큰 레코드이고 dept_no가 ‘d002’인 레코드를 찾는다.
  • 이후 찾은 모든 레코드에 dept_no가 ‘d002’인지 비교하는 작업을 수행한다.

 

 

인덱스의 가용성

  • B-Tree 인덱스의 특징은 왼쪽 값에 기준해서 오른쪽 값이 정렬돼 있다는 것이다.
  • 하나의 칼럼으로 검색해도 값의 왼쪽 부분이 없으면 인덱스 레인지 스캔 방식의 검색이 불가능하다.
SELECT * FROM employees WHERE first_name LIKE '%mer1';
  • 이 쿼리는 인덱스를 못탄다
    • first_name 컬럼에 저장된 값의 왼쪽부터 비교해가며 일치하는 레코드를 찾아야 하는데,
    • 조건 값의 왼쪽 부분(’%mer1’)이 정해져 있지 않기 때문이다
SELECT * FROM dept_emp WHERE emp_no>=10144;
  • 인덱스가 (dep_no, emp_no)로 생성되어 있다면 아래의 쿼리는 인덱스를 효율적으로 사용하지 못한다
  • 다중 컬럼 인덱스로 생성된 인덱스이므로 dept_no를 먼저 정렬한 후, 다시 emp_no 컬럼값으로 정렬돼 있기 때문 (랜덤IO)

 

B-tree에서 인덱스를 효율적으로 사용하지 못하는 케이스

  • NOT-EQUAL로 비교된 경우 (NOT IN, NOT BETWEEN, IS NOT NULL)
    • WHERE column <> 'N'
    • WHERE column NOT IN (10,11,12)
    • WHERE column IS NOT NULL
  • LIKE %xxx 형태 문자열 패턴 비교인 경우
    • WHERE column LIKE '%test‘
    • WHERE column LIKE '%test%‘
    • WHERE column LIKE '_test‘
  • 스토어드 함수나 다른 연산자로 인덱스 컬럼이 변형된 후 비교된 경우
    • WHERE SUBSTRING(column,1, 1) = 'X'
    • WHERE DAYOFMONTH(column) = 1
  • 인덱스 컬럼의 타입을 변환해야 비교가 가능한 경우
    • WHERE char_column = 10 → char를 int와 비교
  • 문자열 데이터 타입의 콜레이션이 다른 경우
    • WHERE utf8_bin_char_column = euckr_bin_char_column

 

 

클러스터링 인덱스

  • pk가 비슷한 레코드끼리 묶어서 인덱스로 저장한 것
  • 클러스터링 인덱스의 리프노드에는 모든 레코드의 컬럼이 저장되어있다
  • 클러스터링이란
    • 여러개를 하나로 묶는다는 의미로 주로 사용됨
    • MySQL 서버에서 클러스터링은 테이블의 레코드를 비슷한 것을 기준으로 묶어서 저장하는 형태
    • 비슷한 것 = pk
    • pk설정시 그 컬럼은 자동으로 클러스터링 인덱스가 된다
    • 주로 비슷한 값을 동시에 조회하는 경우가 많다는 점에서 착안된 설계
    • 테이블의 레코드가 pk 기준으로 정렬되어 저장되는 경우를 → 클러스터링 인덱스 or 클러스터링 테이블
  • pk에 따라 레코드의 저장위치가 결정되므로 테이블의 레코드 저장 방식으로 볼 수 있다
    • 인덱스 자체의 리프노드가 곧 데이터다. → 테이블 자체가 인덱스다
      • 클러스터 인덱스는 페이지를 알기 때문에 바로 그 페이지를 펴는 것
      • 넌 클러스터 인덱스는 뒤에 목차에서 찾고자 하는 내용의 페이지를 찾고 그 페이지로 이동하는 것.
      • 테이블 스캔은 처음부터 한 장씩 넘기면서 내용을 찾는 것
  • pk가 없다면 다음 기준으로 이노디비 스토리지 엔진이 pk를 대체할 컬럼을 선정한다
    • pk가 있다면 pk를 클러스터링 키로 선택
    • not null인 유니크 인덱스중에 첫번째를 클러스터링 키로 선택
    • 자동으로 유니크한 값을 가지도록 증가되는 컬럼을 내부적으로 고른 후 클러스터링 키로 선택
  • 장점 (주로 빠른 읽기)
    • pk(클러스터링키)로 검색할때 성능이 매우 빠름(특히 pk를 범위검색에 사용하는 경우)
    • 테이블의 모든 세컨더리 인덱스가 pk를 가지고있기 때문에 인덱스만으로 쿼리를 처리할 수 있을 확률이 높음(커버링 인덱스)
  • 단점 (주로 느린 쓰기)
    • 테이블의 모든 세컨더리 인덱스가 pk를 가지고있기 때문에 클러스터링 키값이 클수록 전체적으로 인덱스의 크기가 커짐
    • 세컨더리 인덱스로 검색할때 pk로 한번 다시 검색해야하기에 처리성능이 느림
    • INSERT를 할 때 pk에 의해 레코드 저장 위치가 결정되기에 처리 성능이 느림
    • pk를 변경할 때 레코드를 DELETE하고 INSERT해야하기에 처리성능이 느림

  • 클러스터링 인덱스는 인덱스에 실제 데이터를 저장하고있음
  • 논 클러스터링(세컨더리) 인덱스는 인덱스 페이지를 별도로 관리함
    • 논 클러스터링 인덱스의 장단점
      • 장점
        • 실제 데이터 페이지는 정렬되지 않기에 INSERT, UPDATE, DELETE 성능 좋음
      • 단점
        • 인덱스만 정렬되고 실제 데이터는 정렬되지 않기에 클러스터링 인덱스에 비해 검색 느림

 

논 클러스터링 인덱스(세컨더리 인덱스)

  • 클러스터링 인덱스(PK 인덱스)를 제외한 모든 인덱스
  • 리프 노드에서 레코드의 물리적인 주소값이 아닌 PK를 가지기 때문에 레코드를 접근할 때 바로 접근할 수 없다
  • 왜 논 클러스터링 인덱스를 통한 검색은 클러스터링 인덱스를 한번 거치게 했을까?
    • 레코드 변경시 인덱스의 부하를 줄이기 위함
    • pk가 변경되면 레코드의 물리적 주소가 바뀜
    • 논 클러스터링 인덱스는 실제 레코드 주소값(pk)이 아닌 논리적인 pk만 참조하기에 레코드의 값이 변경되어도 논 클러스터링 인덱스는 바뀌지 않는다

클러스터링 인덱스 → pk(데이터)

논 클러스터링 인덱스 → 클러스터링 인덱스 → pk(데이터)

 

  • 클러스터링 인덱스: 인덱스를 가지고 리프노드까지 탐색해서 원하는 데이터를 바로 얻음
  • 논 클러스터링 인덱스: 인덱스를 가지고 리프노드에 탐색해서 실제 데이터 위치를 얻음 → 실제 데이터는 힙영역에서 얻는다

 

클러스터링 테이블 사용시 주의사항

  • 이노디비 테이블(클러스터링 테이블)에서는 주의할 것이 있다
  • 모든 논 클러스터링(세컨더리) 인덱스가 pk를 포함한다
  • 그래서 pk의 크기가 커지면 세컨더리 인덱스도 커진다
  • 그래서 다음과 같은 주의사항들이 있다
    • pk는 auto_increment 보다는 업무적인 컬럼으로 생성
      • 이노디비에서 pk는 클러스터링 키로 사용되며, 이 값에 따라서 레코드의 위치가 결정됨
      • 즉, pk로 검색하는 경우 클러스터링 되지 않은 테이블에 비해서 매우 빠르다는 뜻
      • 따라서 검색에서 빈번하게 사용되고 → 업무적으로 pk가 해당 레코드를 대표할 수 있다면 pk로 설정하는 것이 좋다
    • pk는 반드시 명시할 것
      • 명시하지 않으면 이노디비 스토리지 엔진이 내부적으로 일련번호 컬럼을 추가한다
      • 하지만 이 컬럼은 보이지 않기에 사용자가 접근할 수 없다
      • 따라서 명시하지 않아도 일련번호 컬럼이 추가되기에 명시하는거나 다름없다(오히려 접근이 안되기에 사용불가라는 단점만 있음)
    • auto_increment 컬럼을 인조 식별자로 사용할 경우
      • 세컨더리 인덱스도 필요하고 프라이머리 키의 크기도 길다면 AUTO-INCREMENT 칼럼을 추가하고 이를 프라이머리 키로 설정하면 된다.
      • 이를 인조 식별자라고 함

 

 

유니크 인덱스

  • 같은 값이 2개 이상 저장될 수 없음을 의미
  • 인덱스라기보단 제약조건에 가깝다 → 하지만 인덱스 없이 유니크 제약만 설정할 방법이 없다
  • 유니크 인덱스가 걸린 컬럼에 null도 저장될 수 있지만 null은 중복 허용임
  • 유니크 인덱스가 다른 세컨더리 인덱스보다 조회가 빠른가?
    • 그렇긴한데 유니크”인덱스”라서 빠른건 아니다
    • 인덱스의 성격이 유니크한지 아닌지의 차이이지 인덱스 자체가 동작하는 방식은 동일하다
      • 유니크하지 않은 세컨더리 인덱스는 읽어야 할 값이 많아서 느린것
      • 유니크 인덱스는 1개뿐이라서 빠른것 (1개뿐임이 보장되기에 실행계획이 다름)
      • 인덱스 자체에 대한 차이는 아님
  • 주의
    • 유니크 인덱스는 세컨더리 인덱스보다 변경에 느림
    • 일반인덱스와 유니크인덱스는 인덱스 자체로는 동일하다
    • pk를 유니크 인덱스로 쓰지말자 중복이다

 

 

참고:

 

'DB' 카테고리의 다른 글

[Typeorm] save() 알차게 사용하기  (1) 2023.04.01
[DB] 데이터베이스 정규화  (0) 2021.07.14

자바는 버전별로 어떤 차이가 있나요?

  • 1.5
    • Generic 도입
    • Enum 도입
    • Annotation 도입
  • 1.6
    • G1GC를 테스트용으로 사용하도록 추가함
  • 1.7
    • 다이아몬드 오퍼레이터(<>) 도입
      • 변수선언시 동일한 타입으로 선언하고 싶다면 타입 명시하지 않아도 되고, <>로만 사용하면 됨
  • 1.8
    • 람다식 도입
    • 디폴트 메서드 도입
    • 옵셔널 도입
    • 스트림 도입
  • 9
    • java.net.http 패키지 도입
    • 스트림 api 추가됨
    • 인터페이스에서 private 메서드 사용 가능
    • try with resources 도입
  • 10
    • var 키워드 도입 -> 로컬변수 타입추론
  • 11
    • String, File API 개선
    • 람다식 파라미터로 var 사용가능하도록 개선
  • 12
    • Switch문 개선 "->"사용하는 식으로
  • 13
    • Switch문이 값을 return할 수 있도록 yield 키워드 추가
    • 줄바꿈된 문자열을 자동으로 선언할 수 있는 문법 (""" 어쩌구 저쩌구 """)
  • 14
    • record 타입 추가
    • NPE 에러내용이 더욱 상세하게 나옴
  • 15
    • sealed class
      • 다른 클래스가 상속을 받지 못하도록 제한하는 클래스
  • 16
    • OpenJDK 버전 관리가 Mercurial에서 Git으로 바꾸미
  • 17
    • Pattern Matching for switch
     

 

  • 18
    • default CharSet이 UTF8로 바뀜
    • 기존에는 런타임 환경에 따라 알아서 선택됐었음

자바 Stream API에 대해 설명해주세요

  • 자바8부터 추가된 기능
  • 일련의 데이터의 흐름을 쉽게 처리할 수 있게 도와주는 기능
  • 컬렉션(배열포함)의 요소를 함수형 연산(람다함수)을 통해 하나씩 참조하며 처리할 수 있게 도와주는 반복자

 

Stream API의 특징을 알려주세요

  • 원본 데이터를 변경하지 않는다
  • 일회용이다 -> 한번 사용이 끝나면 재사용 불가 -> 닫힌 스트림 사용시 IllefalStateException 발생
  • 내부 반복으로 작업 처리 -> for, while과 다르게 반복문법을 내부에 숨기고있기에 간결하고 안전한 코드

 

Stream API의 처리 단계를 알려주세요

  • 생성
    • Stream 객체를 생성하는 단계
  • 가공
    • 중간 데이터를 연산하는 과정
  • 결과만들기
    • 가공된 데이터를 원하는 형태로 출력함

TBD...

배열의 중복제거를 위한 방법을 알려주세요

  • Set을 사용
    • Set에 넣으면 중복제거됨
  • ArrayList를 사용
    • ArrayList의 contain을 사용하여 중복확인 후 중복이 제거된 새로운 리스트 생성
  • Stream의 distinct() 사용

자바 컬렉션에 대해 설명해주세요

  • 데이터의 집합
  • 자료구조들을 자바 클래스로 구현해둔 것
  • C에서는 LinkedList를 사용하려면 직접 코드짜야하지만
  • 자바에서는 컬랙션을 인스턴스화하면 된다

 

컬렉션의 장점은 뭐죠?

  • 표준화되어있기에 재사용성이 높다
  • 검증된 성능과 품질
  • 이미 구현되어있는것을 갖다쓰는거기에 편리

 

컬렉션은 어떻게 구성되어있죠?

 

  • 컬랙션 프레임워크는 크게 2개로 나뉜다
    • 컬렉션 인터페이스
    • 맵 인터페이스
  • 맵이 따로인 이유는
    • 두개의 데이터를 묶어서 한쌍으로 다루기에

 

Iterator 인터페이스는 뭐죠?

  • 컬렉션 인터페이스들보다 상위 최상위 인터페이스
  • 이터레이터 객체를 관리하는 인터페이스

https://inpa.tistory.com/entry/JCF-%F0%9F%A7%B1-Collections-Framework-%EC%A2%85%EB%A5%98-%EC%B4%9D%EC%A0%95%EB%A6%AC

 

 

MVC 패턴에 대해 설명해주세요

  • 디자인 패턴중에 하나다
  • Model, View, Controller

  • 사용자가 Controller 조작
  • Controller는 Model에서 데이터를 가져옴
  • View를 제어하여 사용자에게 전달
  • Model이란?
    • DB, 변수, 상수 등의 데이터
  • View란?
    • 텍스트박스, 체크박스 등 유저 인터페이스
    • 출력을 담당
    • 사용자들이 보는 화면
  • Controller란?
    • 사용자와 데이터를 연결하는 다리역할
    • 사용자가 무언가를 클릭하거나 수정하는 이벤트를 처리하는 부분
  • MVC 패턴을 사용해야하는 이유
    • 각각 기능에 집중할 수 있다
    • 확장성 유연성 증가

객체지향에 대해 설명해주세요

  • 객체란? -> 현실에 존재하는 대상
  • 프로그래밍에서 필요한 데이터를 추상화시켜 상태행위를 가진 객체를 만들고
  • 그 객체들 간의 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍 방법론

 

객체지향의 장단점이 뭐죠?

  • 장점은
    • 코드 재사용성이 좋다. 상속을 통한 확장도 가능
    • 유지보수가 쉽다. 절차지향에서는 일일이 찾아서 수정해야하지만, 객체지향에서는 클래스 내부를 보면 된다
    • 대형 프로젝트에 적합하다. 클래스단위로 모듈화 가능이기에 협업에 용이
  • 단점은
    • 개발속도가 상대적으로 느리다
    • 설계하기 쉽지 않다

 

클래스와 인스턴스가 뭐죠?

  • 클래스는
    • 대상의 속성과 행위를 변수와 메서드로 정의한 것
  • 인스턴스는
    • 정의한 클래스를 실제 메모리에 할당시키는 것

 

객체지향의 특성은 뭐가있죠?

  • 캡슐화
    • 클래스라는 캡슐에 속성과 행위를 묶고, 외부로부터 보호하는 것
      • 데이터 보호
      • 데이터 은닉 -> 필요한 부분만 노출
    • 접근제어자
    • 게터, 세터 -> 결합도를 낮출 수 있다
  • 상속
    • 기존 클래스를 재활용하여 새로운 클래스를 만드는 것
    • 속성과 행위를 물려받음
    • 중복을 제거하고 필요한 부분만 수정할 수 있다
  • 추상화
    • 객체지향 관점에서 클래스를 정의하는 것
    • 객체의 본질적이고 공통적인 부분만 추출하여 정의하는 것
      • 추상클래스
      • 인터페이스
  • 다형성
    • 객체의 속성이나 행위가 상황에 따라 여러가지 형태를 가질 수 있는 것
    • 상위클래스의 참조변수가 하위클래스 객체를 참조하는 것
      • 오버로딩
      • 오버라이딩
        • 오버로딩 오버라이딩처럼 메서드가 상황에따라 다르게 동작할 수 있고
      • 아래 사진처럼 상위클래스 변수가 하위클래스 변수를 받을 수 있는것
      • 이것이 다형성

 

 

 

참고:

https://www.codestates.com/blog/content/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%ED%8A%B9%EC%A7%95

 

객체 지향 프로그래밍의 4가지 특징ㅣ추상화, 상속, 다형성, 캡슐화 -

객체 지향 프로그래밍은 객체의 유기적인 협력과 결합으로 파악하고자 하는 컴퓨터 프로그래밍의 패러다임을 의미합니다. 객체 지향 프로그래밍의 기본적인 개념과 그 설계를 바르게 하기 위

www.codestates.com

 

정말 오오오오오랜만에 글을쓴다

다시 잘 써보자고


가비지란?

  • JVM의 힙영역에 할당됐던 메모리중 필요없게 된 메모리를 가비지라고 한다
  • C언어의 경우 free() 함수를 통해 직접 메모리 해제
  • 자바는 gc가 해준다
Person person = new Person();
person.setName("lsh");
person = null;

// 가비지 발생
person = new Person();
person.setName("ksh");
 

가비지 컬렉션 장점

  • 개발자가 완벽하게 신경쓰지 않아도 된다

가비지 컬렉션 단점

  • 언제 메모리가 해제될지 개발자가 알 수 없다
  • GC 동작중에는 STW, 오버헤드 발생

 

 

GC란

  • 힙 영역에서 유효하지 않은(사용하지 않는) 메모리를 자동으로 수거하는 기능
  • 힙 영역은 다음 두가지가 전제가 된다
    • 대부분의 객체는 금방 unreachable 상태가 된다
    • 오래된 객체 -> 새로운 객체로의 참조는 아주 적다
  • 따라서 힙 영역은 객체의 생존 기간에 따라 Young 영역과 Old 영역으로 나눔

young영역

  1. young은 eden과 survivor1, 2로 구성
    1. eden
      1. eden은 new를 통해 새로 생성된 객체가 위치함
      2. 정기적인 gc 수행 후 살아남은 객체들은 survivor1,2로 넘김
    2. survivor
      1. 최소 1번 이상의 gc에서 살아남은 객체가 위치한 영역
      2. survivor1이나 survivor2중 하나는 꼭 비어있어야 한다
  2. 이곳에서의 gc 동작을 Minor gc라고함

 

old영역

  • young영역에서 reachable 상태를 유지하여 살아남은 객체가 복사되어 이곳에 위치하게된다
  • 이곳에서의 gc를 메이저gc 혹은 full gc라고함

  • card table
    • 예외적으로 old영역에 있는 객체가 Young영역에 있는 객체를 참조하는 경우도 있다
    • 이럴때를 대비하여 512바이트의 덩어리(청크)로 되어있는 카드테이블이라는 것이 있다
    • 마이너 gc가 발생할때 old영역에서 참조하는 young으로 참조하는 객체가 있는지 old영역을 확인안해도 되기에 효율적

 

왜 굳이 영역을 나누는가?

  • heap 영역을 나누는 이유는 heap 전체를 탐색하여 메모리를 해제하는 full gc로 인한 성능상의 이슈를 최소화 시키기 위함
    • weak generational hypothesis의 장점을 극대화 시키기 위함
    • 주로 old 영역의 객체는 크기가 큰 것들이 대부분이고 gc 소요시간이 Minor gc보다 오래걸린다
      • Minor gc와 Major gc의 비율간의 트레이드 오프
  • survivor 영역을 두개로 나누는 이유는 메모리 단편화 문제를 해결하기 위함

내부단편화

 

외부단편화

 

 

 

GC 동작 방식

  • young 영역과 old 영역의 세부적인 동작방식은 다르나 다음 두가지는 공통이다
    • Stop the world
      • gc 실행을 위해 jvm이 애플리케이션의 실행을 멈추는 작업
      • gc를 실행하는 쓰레드를 제외한 모든 스레드가 중단됨
      • gc튜닝은 대부분 이 시간을 줄이는것
    • Mark and sweep
      • 마크 -> 사용되는 메모리와 그렇지 않은 메모리를 식별하는 작업
      • 스윕 -> 마크단계에서 사용되지 않는것으로 판별된 메모리를 해제하는 작업
      • 컴팩션 -> 파편화된 메모리 영역을 앞에서부터 채워나가는 작업

 

Minor GC 동작 방식

사진출처: https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98GC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC

 

☕ 가비지 컬렉션 동작 원리 & GC 종류 💯 총정리

Garbage Collection(GC) 이란? 가비지 컬렉션(Garbage Collection, 이하 GC)은 자바의 메모리 관리 방법 중의 하나로 JVM(자바 가상 머신)의 Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 메모리 객

inpa.tistory.com

 

 

age란?

  • Survivor 영역에서 객체가 gc로부터 살아남은 횟수
  • 임계점에 다다르면 Promotion되어 old영역으로 이동한다
  • JVM중 일반적인 HotSpot JVM의 경우 age 임계값이 31이다
 

 

 

 

Major GC 동작 방식

  • young에서 age가 차서 넘어온 객체들
  • old 영역의 메모리가 부족해지면 major gc 수행
  • old 영역에서 한번에 삭제함
  • old 영역은 young 영역보다 상대적으로 큰 공간을 가지고있기에 gc 수행시간 길다 → STW
  • STW를 줄이기위해 여러 가비지 컬렉션 알고리즘이 존재

Minor Major GC 비교

  • 만약 gc가 동작해도 모든 객체가 reachable해서 삭제될 객체가 없다면? → OOM 발생

 

 

 

GC 알고리즘

Serial GC

  1. 1 core cpu일때 사용하기위함
  2. Gc를 처리하는 스레드가 1개라서 STW시간이 제일 길다
  3. 실무에서 사용하는 케이스가 거의 없다고한다

 

Parallel GC

  • 자바8의 default gc
  • Minor gc를 멀티스레드로 수행함 (Major gc는 여전히 싱글스레드)
  • Serial gc에 비해선 STW 감소
  • 스레드는 기본적으로 cpu 개수만큼 할당됨

 

Parallel old GC

  • Parallel gc를 개선함
  • old영역에서도 멀티스레드로 메이저 gc수행
  • Mark - summary - compact 방식

 

Cms GC

  • 어플리케이션의 쓰레드와 GC 쓰레드가 동시에 실행되어 stop-the-world 시간을 최대한 줄이기 위해 고안된 GC
  • 단, GC 과정이 매우 복잡해짐.
  • GC 대상을 파악하는 과정이 복잡한 여러단계로 수행되기 때문에 다른 GC 대비 CPU 사용량이 높다
  • 메모리 파편화 문제
  • CMS GC는 Java9 버전부터 deprecated 되었고 결국 Java14에서는 사용이 중지

 

G1GC

  • Java 9 이상부터 default GC
  • 기존의 GC 알고리즘에서는 Heap 영역을 물리적으로 고정된 Young / Old 영역으로 나누어 사용하였지만, G1 gc는 아예 이러한 개념을 뒤엎는 Region이라는 개념을 새로 도입하여 사용.
  • 전체 Heap 영역을 Region이라는 영역으로 체스같이 분할하여 상황에 따라 Eden, Survivor, Old 등 역할을 고정이 아닌 동적으로 부여
  • Garbage로 가득찬 영역을 빠르게 회수하여 빈 공간을 확보하므로, 결국 GC 빈도가 줄어드는 효과를 얻게 되는 원리
    • 이전의 GC들처럼 일일히 메모리를 탐색해 객체들을 제거하지 않는다
    • 대신 메모리가 많이 차있는 영역(region)을 인식하는 기능을 통해 메모리가 많이 차있는 영역을 우선적으로 GC 한다. → 영역(region)을 나눠 탐색하고 영역(region)별로 GC가 일어난다.
    • 또한 이전의 GC 들은 Young Generation에 있는 객체들이 GC가 돌때마다 살아남으면 Eden → Survivor0 → Survivor1으로 순차적으로 이동했지만, G1 GC에서는 순차적으로 이동하지는 않는다. 
    • 대신 G1 GC는 더욱 효율적이라고 생각하는 위치로 객체를 Reallocate(재할당) 시킨다. 
    • 예를 들어 Survivor1 영역에 있는 객체가 Eden 영역으로 할당하는 것이 더 효율적이라고 판단될 경우 Eden 영역으로 이동시킨다.

 

ZGC

  1. Java 15에 release됨
  2. 대량의 메모리(8MB ~ 16TB)를 low-latency로 잘 처리하기 위해 디자인 된 GC
  3. G1의 Region 처럼,  ZGC는 ZPage라는 영역을 사용하며, G1의 Region은 크기가 고정인데 비해, ZPage는 2mb 배수로 동적으로 운영됨. (큰 객체가 들어오면 2^ 로 영역을 구성해서 처리)
  4. ZGC가 내세우는 최대 장점 중 하나는 힙 크기가 증가하더도 STW의 시간이 절대 10ms를 넘지 않는다는 것

참고: GC 성능 비교

 

 

G1GC

  • G1은 Garbage First 라는 뜻
  • 조기 승격(Promotion)에 덜 취약하다. → old로 너무 빠르게 이동되는 문제 → STW
  • 대용량 heap에서 확장성(특히 중단시간)이 우수하다.
  • full STW GC를 없애거나 확 줄일 수 있다.
  • Java 9 부터 default GC
  • RSet(Remembered Set)을 통해 어떤 객체가 어떤 리전에 저장되어있는지 추적 가능 → 전체 힙을 뒤질필요가 없어짐

 

G1GC 동작 과정

  1. Initial Mark - SWT : Old 영역에서 존재하는 객체들이 참조하는 Survivor 영역을 찾는다. SWT가 발생한다.
  2. Root Region Scanning : Initial Mark 단계에서 식별한 Survivor 영역에서 Old 영역을 가리키는 레퍼런스를 식별한다.
  3. Concurrent Mark : 힙 전체에 걸쳐 접근 가능한 살아있는 객체를 찾는다.
  4. Remark - STW : Concurrent Mark 단계를 검증하고, 최종적으로 살아남을 객체들을 식별한다. 이 단계에서는 SATB(Snapshot-At-The-Beginning) 알고리즘이 사용된다. STW가 발생한다.
  5. Cleanup - STW : 어플리케이션을 멈추고(STW) 살아있는 객체가 가장 적은 리전에 대한 미사용 객체를 제거한다. 이후 STW를 끝내고, 앞서 GC 과정에서 완전히 비워진 리전을 FreeList에 추가하여 재사용할 수 있게 한다.
  6. Copy : GC 대상 리전이었지만 Cleanup 과정에서 완전히 비워지지 않은 리전의 살아남은 객체들을 새로운 리전에 복사하여 Compaction 작업을 수행한다.

 

gc튜닝

  • 언제 튜닝해야하는가? → 성능저하의 원인이 명백히 gc때문일때
  • 튜닝의 핵심은
    • old영역으로 넘어가는 객체 최소화하기
    • Major gc 시간 짧게 유지하기

즉, Major gc를 적게발생시키고, 발생했다면 빠르게 끝내야함

 

GC튜닝 방법

  • 힙 크기 설정
    • 힙이 크면 gc수행시간이 늘어남, 힙이 작으면 gc가 더 자주 수행됨 -> 잘 조절해야함
  • Young영역과 Old영역 크기 비율 + Eden과 Survivor의 크기 비율
    • old영역이 커지면 → Major gc가 자주발생하지않음 하지만 발생하면 오래걸림

 

 

 

G1GC 튜닝포인트

G1GC가 제공하는 파라미터가 많기에 튜닝 포인트도 많다

  • Maximum GC Pause Time
    • -XX:MaxGCPauseMillis 설정을 통해 GC 실행 중에 최대 일시 중지 시간을 지정함으로써 높은 지연 시간을 최소화 하거나 높은 처리량을 설정할 수 있다.

 

  • Young Gen 사이즈 세팅을 하지 말 것(-XX:MaxGcPauseMillis 설정을 할 경우)
    • -Xmn(new 영역)이나 -XX:NewRatio 설정을 피해야 한다.
    • G1GC 알고리즘은 일시 중지 목표 시간을 충족하기 위해 Young 영역을 임의로 수정하게 되는데 Young 영역을 명시적으로 설정할 경우 일시 중지 목표 설정이 정상적으로 작동하지 않는다.

 

  • 다른 GC 알고리즘에서 사용하던 JVM 인수 제거
    • 기본적으로 G1GC의 경우 다른 GC 알고리즘(Serial, Parallel, CMS)에서 사용하던 JVM 인수와 같이 사용할 경우 G1GC의 파라미터가 정상적으로 동작하지 않을 수 있다.

 

  • 문자열 중복 제거
    • -XX:+UseStringDeduplication 설정을 통해 문자열 중복을 제거 한다.
    • JDK 개발팀의 조사에 따르면 다음과 같은 자바 애플리케이션의 특징이 있다고 한다
      • 프로세스의 25%는 문자열임
      • 그 중 13.5%는 중복 문자열임
      • 평균 문자열의 길이는 45자임
    • 예를들어 멤버리스트 조회시 status가 “ACTIVE”라는 동일한 문자열이나 다 다른 메모리 공간 점유

 

그 외의 주요 파라미터

 

 

 

GC 튜닝보다 메모리 누수 예방이 우선

  • 메모리 누수 : 사용되지 않는 객체들이 힙영역에 남아있는것

Static 변수에 의한 누수

  • Static 변수는 클래스가 로드될때 생성되어 JVM이 종료될때까지 메서드 영역에 남아있는다 (사용 여부와 관계x)
  • Static 변수가 객체를 참조 중이라면 해당 객체는 GC의 대상이 되지 않는다
  • 더 이상 사용하지 않는다면 null을 할당하여 참조를 제거하자

 

무분별한 Autoboxing

public class Adder {
       public long addIncremental(long l)
       {
              Long sum = 0L;
              sum = sum + l;
              return sum;
       }
       public static void main(String[] args) {
              Adder adder = new Adder();
              for(long i ; i < 1000 ; i++) {
                adder.addIncremental(i);
              }
       }
}

 

 

컬렉션 클래스의 데이터를 해제하지 않는 경우

  • List, Map, Set 같은 컬렉션 클래스들에 객체가 담겨있는 경우 객체의 참조가 해제되지 않음
  • null 참조로 해제한다
      public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // 다 쓴 객체 참조 해제
        return result;
    }
  • Weak reference를 사용한다
    • 특정 key 값이 더 이상 사용되지 않는다고 판단되면 해당 Key - Value 쌍을 삭제
  • 자바에서 참조 종류는 다음 4가지가 있다
    • 강한 참조(Strong Reference)
      • 강한 참조는 Java의 기본 참조 유형으로 new 할당 후 새로운 객체를 만들어 해당객체를 참조하는 방식
      • 강함 참조를 통해 참조되고 있는 객체는 참조가 해제되지 않는 이상 가비지 컬렉션의 대상에서 제외된다.
Object obj = new Object();
// 만약 GC 를 원한다면 명시적으로 null 표시를 해줘야 한다.
obj = null;
  • 약한 참조(Weak Reference)
    • 약한 참조는 java의 lang 패키지의 WeakReference 클래스를 사용하여 생성한다.
    • 약한 참조는 GC가 발생하면 무조건 수거된다.
    • WeakReference가 사라지는 시점이 GC의 실행 주기와 일치한다.
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);
obj = null;

System.gc();

// 무조건 null 을 확인하게 된다.
System.out.println(weakRef.get());
  • Soft Reference
    • Soft 참조는 강한 참조와 약한 참조와는 다르게 GC에 의해 수거될 수도 있고, 수거되지 않을 수도 있다.
    • 메모리에 충분한 여유가 있다면 GC가 수행된다 하더라도 수거되지 않는다. 하지만 메모리가 부족하면 수거될 확률이 높다.
Object obj = new Object();
SoftReference<Object> softRef = new SoftReference<>(obj);
obj = null;

System.gc();

// GC 가 여유롭다면 해시코드를 확인할 수 있다.
System.out.println(softRef.get());
  • Phantom Reference?
    • 가장 약한 참조 유형입니다.
    • 객체 수거시에도 참조가 남아있는 참조 유형입니다.
    • 객체의 finalize() 메서드가 호출된 직후에 GC 에 의해 수거됩니다.
    • java.lang.ref PhantomReference class 로 만들 수 있습니다.
    • 생성자에는 넣고자 하는 클래스와 함께 ReferenceQueue 를 인자로 받습니다.
    • PhantomReference 는 객체가 참조되지 않습니다.
    • 객체의 finalize 메서드가 호출된 직후 Phantom Reference 가 ReferenceQueue 에 등록됩니다.
    • 이를 통해 객체의 finalize() 메서드가 호출되었음을 알 수 있습니다.
    • 일반적으로 Phantom Reference 는 Native 객체나 Direct Memory 와 같이 JVM 에서 관리되지 않는 자원들을 해제하기 위해 사용됩니다.

 

CustomKey 사용으로 인한 누수

  • Map을 사용할 때 custom key를 사용할 때는 equals()와 hashcode()를 값을 기반으로 구현해야한다
  • 아래의 경우 Key값이 같은 객체로 인식하지 못해서 계속 Map에 쌓이게 되면서 메모리를 점유하게 된다
public class CustomKey {
    private String name;
    
    public CustomKey(String name) {
        this.name=name;
    }
    
    public staticvoid main(String[] args) {
        Map<CustomKey,String> map = new HashMap<CustomKey,String>();
        map.put(new CustomKey("Shamik"), "Shamik Mitra");
   }
}

 

 

해제되지 않은 리소스로 인한 누수

  • close()나 try with resources 구문으로 리소스 반환해야함
public class Main {
    public static void main(String[] args) {
        try (FileWriter file = new FileWriter("data.txt")) { // 파일을 열고 모두 사용되면 자동으로 닫아준다
            file.write("Hello World");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

+ Recent posts