1. 구구의 [달리는 기차의 바퀴 교체하기] 강의를 들었다.
- 유지보수, 리팩터링 역량의 필요성
- 점진적인 리팩터링 전략에 대한 이해
유지보수 역량이 필요하다.
- (초창기) 배달의민족은 대학생도 만들 수 있다.
- 만드는 건 누구나 한다.
- 서비스를 유지하고 발전시켜 나가는 게 어렵다.
- 기업들은 서비스 개발 마인드를 가진 개발자가 필요하다.
- 서비스를 지속적으로 발전시키기 위해 필요한 역량은?
- 협업
- 유지보수 (리팩터링)
달리는 기차의 바퀴를 갈아끼운다.
- 처음부터 완벽하게 만들 수 없다. 왜?
- 시장 상황은 계속 바뀐다.
- 참여자들의 역량도 변한다.
- 기술이 발전한다. 등등등
- 달리는 기차의 바퀴를 바꿀 수 있어야 한다. 어떻게?
- 점진적으로 리팩터링 하는 미션으로 연습한다
빅뱅전략
- 빅뱅 방식으로 백날 해봐야, 빅뱅만 나올 뿐이다 - 마틴 파울러
점진적인 리팩터링 전략
- 특정 기능을 새로운 애플리케이션 및 서비스로 점진적으로 교체하여 레거시 시스템을 단계적으로 마이그레이션한다.
- 레거시 시스템의 기능이 교체되면 결국 새 시스템이 기존 시스템의 모든 기능을 대체하여 기존 시스템을 중단하고 서비스 해제할 수 있다.
=> 빅뱅전략말고 점진적인 리팩터링 전략! (작은규모의 실패)
자바에서 자바로 변경할 때 인터페이스를 이용해 점진적으로 개선 가능
과도기 : 리팩토링 과정 중에 레거시 코드와 새로운 코드가 공존하는 시기
이번 MVC미션 관련~~
Legacy MVC의 문제점
- 새로운 controller생길 때마다 RequestMapping에 url & controller 추가해줘야함
- RequestMapping 클래스를 수정하면 MVC 프레임워크 영역까지 수정하게 된다
=> 어노테이션 기반의 MVC 추가! (1단계)
=> Legact MVC와 @MVC 통합 (2단계)
2. 톰캣의 스레드 관련 자료를 조금 봤다.
[Tomcat] maxThreads, maxConnections, acceptCount로 Tomcat 튜닝하기
스레드 풀에는 최대 maxThreads 값만큼의 스레드가 존재
이 곳에 있는 thread들이 Connector에 의해 관리되는 queue에 존재하는 커넥션 요청들을 처리
만약 Connector 내부적으로 관리하는 큐의 크기(maxConnection)을 넘어선 많은 요청이 발생하면, 운영체제가 관리하는 큐로 요청 이동
이 경우 커넥션 요청은 수락되지만 처리되지는 않음
- maxThreads
- Tomcat의 '최대' 스레드 개수
- Http Connector에서 처리할 수 있는 최대 요청 처리 수
- 즉, 동시에 얼마나 많은 요청을 처리할 수 있느냐
- ex) maxThreads = 10 이면 한번에 처리할 수 있는 최대 요청 개수 10
- maxConnections
- 서버가 주어진 시간에 수락하고 처리할 수 있는 최대 커넥션 개수
- 이렇게 수락 및 처리되는 커넥션들은 Tomcat Connector가 관리하는 큐에 저장
- 만약 커넥션 요청이 너무 많이 들어와서 maxConnections 값보다 더 많이 들어오면,
현재 수락 및 처리중인 커넥션 개수가 maxConnections 값 이하로 떨어질 때까지 다른 요청들은 전부 운영체제가 관리하는 큐로 이동해 대기
- acceptCount
- maxConnections 값 이상의 커넥션 요청이 들오면 따로 운영체제가 관리하는 큐에서 대기
-> 이 큐의 사이즈 - 이 큐에 들어온 커넥션 요청들은 '수락'만 된 상태이고 '처리'는 되지 않음
- maxConnections 값 이상의 커넥션 요청이 들오면 따로 운영체제가 관리하는 큐에서 대기
maxConnection < maxThreads : maxConnection만큼 처리 -> 노는 스레드 발생
maxConnection > maxThreads : maxThreads만큼 처리 -> 모든 스레드 열일
maxConnection < 총 요청 수 : maxConnection + acceptCount 만 처리됨 (나머지 처리 불가)
스프링부트는 어떻게 다중 유저 요청을 처리할까? (Tomcat9.0 Thread Pool)
사실 스프링부트가 다중요청을 처리하는 것이 아니라,
스프링 부트에 내장되어 있는 서블릿 컨테이너(Tomcat)에서 다중요청을 처리해준다!
(java의 thread pool 클래스와 매우 유사한 자체 스레드 풀 구현체를 가지고 있다)
- 스프링부트는 내장 서블릿 컨테이너인 Tomcat을 이용합니다.
- Tomcat은 다중 요청을 처리하기 위해서, 부팅할 때 스레드의 컬렉션인 Thread Pool을 생성합니다.
- 유저 요청(HttpServletRequest)이 들어오면 Thread Pool에서 하나씩 Thread를 할당합니다.
해당 Thread에서 스프링부트에서 작성한 Dispatcher Servlet을 거쳐 유저 요청을 처리합니다. - 작업을 모두 수행하고 나면 스레드는 스레드풀로 반환됩니다.
application.yml
server:
tomcat:
threads:
max: 200 # 생성할 수 있는 thread의 총 개수
min-spare: 10 # 항상 활성화 되어있는(idle) thread의 개수
max-connections: 8192 # 수립가능한 connection의 총 개수
accept-count: 100 # 작업큐의 사이즈
connection-timeout: 20000 # timeout 판단 기준 시간, 20초
port: 8080 # 서버를 띄울 포트번호
이 글 너무 어렵다 ㅋㅋ .. ㅠ 나중에 다시 읽어봐야할듯하다.
maxConnections : 동시에 처리할 수 있는 최대 Connection의 개수 (사실상 서버의 실질적인 동시 요청 처리 개수)
NIO에서는 maxThreads보다 많은 양의 Connection을 유지할 수 있다.
NIO에서는 maxThreads보다 적거나 같은 수의 maxConnections을 설정하는 것은 비효율적
BIO : 1 Connection - 1 Thread (반드시 maxConnections = maxThread 여야함)
NIO : N Connection - 1 Thread (maxConnection > maxThread도 동작가능)
acceptCount : maxConnections 이상의 요청이 들어왔을 때 사용하는 요청 대기열 queue의 사이즈
스레드 풀은 왜 쓰는 걸까요? 어떻게 쓰는게 잘 쓰는 걸까요?
큐에 제한 없으면, 요청 계속 쌓임 -> 메모리 고갈 위험
=> queue size 제한 반드시 둬야함!!! 그 이후 들어오는 요청 버리더라도. 전체 시스템 지켜야하니까.
Executors.newFixedThreadPool의 queue는 Integer.MAX_VALUE
즉, 제한이 없다.
-> 요청이 많으면 queue에 계속 쌓일 수 있다.
i/o : 데이터의 입출력
네트워크 통신은 socket을 통해 데이터가 입출력된다.
backend server : 네트워크 상의 요청자들과 각각 소켓을 열고 통신
block I/O : I/O 작업을 요청한 프로세스/스레드는 요청이 완료될 때까지 블락됨
non-block I/O : 프로세스/스레드를 블락시키지 않고 요청에 대한 현재 상태를 즉시 리턴
-> 블락되지 않고 즉시 리턴하기 때문에 스레드가 다른 작업을 수행할 수 있다
3. [펀잇] 상품 목록 조회 API 수정 (개선?)
📍 최초 : join (reviewCount 정규화) + Page
- join해서 reviewCount 가져온다
- reviewCount 기준 정렬로 인해 service에 분기 + repository에 메소드 2개
1) 리뷰수 많은 순 정렬 : 70ms
쿼리 수 : 3
2) 가격 낮은 순 정렬 : 73ms
쿼리 수 : 3
📍 1차 개선 : reviewCount 반정규화 + Slice
- 펀잇의 경우 무한스크롤이기 때문에 페이징에 대한 자세한 정보 필요 없음
-> Page대신 Slice로 받아와 hasNext값과 데이터만 프론트로 전달 - service 분기 & repository 메소드2개 통합
- join 제거
1) 리뷰수 많은 순 정렬 : 39ms
쿼리 수 : 2
2) 가격 낮은 순 정렬 : 46ms
쿼리 수 : 2
(Page대신 Slice를 사용하여 count 쿼리 안나감)
📍 2차 개선 : 인덱스
select p.id, p.name, p.price, p.image, p.average_rating, p.review_count
from product p
where p.category_id=2
order by
p.price asc ,
p.id DESC
limit 10 offset 100;
요 쿼리를 기준으로 인덱스를 짜봤다. (어차피 나머지 정렬 조건도 비슷하니까)
1) 아무런 인덱스도 걸지 않았을 때
- FK인 category_id 인덱스만 탄다
- using filesort -> 얘를 제거해야해!
2) CREATE INDEX categoryid_price_id ON product (category_id, price, id);
- 새로만든 인덱스를 타지만 여전히 using filesort
3) CREATE INDEX categoryid_price_asc_id_desc ON product (category_id, price ASC, id DESC);
- 드디어 filesort 제거!
3)으로 인덱스를 걸고 요청을 쐈을 때 무려 7ms만에 응답이 돌아오더라..!!
4. 인덱스와 실행계획
📍 테이블에 존재하는 index 조회하는 법
SHOW INDEX FROM <테이블명>;
📍 index 추가하는 법
CREATE INDEX <인덱스명> ON <테이블명> (칼럼명1, 칼럼명2, ...);
📍 Extra : using filesort
Using filesort : ORDER BY를 처리하기 위해 인덱스를 이용할 수도 있지만 적절한 인덱스를 사용하지 못하는 경우, MySQL에서 정렬을 한 번 해야하는데 소트 버퍼에 레코드를 복사해서 정렬하는 비효율적인 작업을 하는 것을 의미한다. 실무에서 정렬은 거의 필수라 그런지 제일 자주 보이는 비효율적인 케이스라고 볼 수 있다. (= 튜닝 대상이 된다.)
using temporary, using filesort
https://jaehoney.tistory.com/192
"using filesort"는 MySQL에서 정렬을 위해 디스크 파일 정렬을 사용할 때 실행 계획에서 나타나는 표현입니다. 이는 데이터베이스가 인덱스를 사용하지 않고 데이터를 디스크에서 메모리로 읽어와 정렬 작업을 수행할 때 발생합니다. 따라서 "using filesort"가 나타나면 정렬이 비효율적으로 수행되고 있음을 나타냅니다.
using index condition
https://jojoldu.tistory.com/474
위의 두 블로그 조만간 좀 더 자세히 봐야할 것 같다!
기타
모놀리스 vs 마이크로서비스
감정회고
흑흑 아 오늘 거의 11시까지 남아서 상품목록조회 api 수정+개선했는데..
7ms까지 만들고 짱 신났는데..
생각해보니 커서기반페이징(노오프셋)으로도 바꿔야한다!
+ 그러면 첫번째 페이지 분기해야해서 동적쿼리 처리도 해야한다..
오엠지~~
해야할 일
- 조회api 튜닝 (+인덱싱)
- 상품목록조회 api
- 레시피상세조회 api
- 레시피목록조회 api
- 쿼리정리 (n+1 등 처리는 다음주에)
- 위의 세개
- 레시피작성 api
- 레시피좋아요 api
- 톰캣 튜닝 시나리오 짜기
- 부하테스트? 성능테스트? 좀 알아보기. 뭘 기준으로 봐야할까?
나 그리고 심지어 금요일날에 있는 5차 데모데이 발표자다 ><
수목 짱바쁠 예정!!