2022-08-24-TIL
2022-08-24-TIL
Today I Learned
Instagram Architecture
인스타그램 설계는 시스템 설계 면접에서 자주 나오는 주제다. 요구사항은 사진 업로드와 조회, 팔로우, 팔로우한 사람들의 게시물을 모은 뉴스피드 정도로 단순하지만, 읽기가 쓰기보다 훨씬 많다는 특성 때문에 설계 포인트가 생긴다.
- 사진 파일은 오브젝트 스토리지에, 게시물·사용자·팔로우 관계 같은 메타데이터는 DB에 나눠 저장하고, 이미지는 CDN으로 서빙한다.
- 업로드(쓰기)와 조회(읽기)는 부하 특성이 달라 서비스를 분리해서 따로 확장한다.
- 뉴스피드는 조회 시점에 팔로우 목록의 게시물을 모으면 느리므로, 게시 시점에 팔로워들의 피드에 미리 넣어 두는 방식(fan-out on write)과 조회 시점에 모으는 방식(fan-out on read)을 섞는다. 팔로워가 매우 많은 계정까지 미리 넣으면 쓰기가 폭증하기 때문이다.
두 번째 링크는 인스타그램 엔지니어링 블로그의 검색 아키텍처 글이다.
- https://www.educative.io/courses/grokking-the-system-design-interview/m2yDVZnQ8lG
- https://instagram-engineering.com/search-architecture-eeb34a936d3a
New Features in Java 17
JDK 17은 2021년 9월 14일 GA된 LTS 릴리스다. 주요 변경은 다음과 같다(OpenJDK: JDK 17).
- Sealed Classes 정식 도입:
sealed,permits로 어떤 클래스가 상속할 수 있는지 제한한다. - Pattern Matching for switch(프리뷰)
- Foreign Function & Memory API(인큐베이터), Vector API(2차 인큐베이터)
- macOS/AArch64 포트: Apple Silicon 지원
- 제거·폐기: RMI Activation과 실험적 AOT/JIT 컴파일러 제거, Applet API와 Security Manager는 제거 예정(deprecated for removal)
1
2
3
public sealed interface Shape permits Circle, Square {}
public record Circle(double r) implements Shape {}
public record Square(double side) implements Shape {}
- https://www.baeldung.com/java-17-new-features
- https://mkyong.com/java/what-is-new-in-java-17/
JPA N+1 Query Problem
N+1 문제는 목록을 조회하는 쿼리 1번 뒤에, 각 행의 연관 엔티티를 가져오는 쿼리가 행 수(N)만큼 추가로 나가는 현상이다. 팀 목록 10개를 조회한 뒤 각 팀의 회원 컬렉션에 접근하면 회원 조회 쿼리가 10번 더 나간다. 지연 로딩이면 컬렉션에 접근하는 시점에, 즉시 로딩이어도 JPQL로 조회하면 엔티티를 먼저 가져온 뒤 연관 엔티티를 따로 채우면서 발생한다. 그래서 즉시 로딩으로 바꾼다고 해결되지 않는다.
해결 방법은 상황에 따라 고른다.
- fetch join(
join fetch)이나@EntityGraph: 연관 엔티티를 한 쿼리로 함께 가져온다. 컬렉션 fetch join은 행이 늘어나 페이징을 DB에서 할 수 없다는 제약이 있다. @BatchSize또는hibernate.default_batch_fetch_size: 연관 엔티티를IN절로 묶어 가져와 N번을 N/배치 크기 번으로 줄인다. 컬렉션 페이징과 함께 쓰기 좋다.DTO 프로젝션: 필요한 컬럼만 조인 쿼리로 바로 조회한다.
- https://medium.com/geekculture/jpa-entitygraphs-a-solution-to-n-1-query-problem-e29c28abe5fb#:~:text=The%20N%2B1%20query%20problem%20is%20said%20to%20occur%20when,the%20performance%20of%20the%20application.
- https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping#:~:text=The%20N%2B1%20query%20problem%20happens%20when%20the%20data%20access,the%20larger%20the%20performance%20impact.
- https://medium.com/doctolib/understanding-and-fixing-n-1-query-30623109fe89
- https://zetawiki.com/wiki/N%2B1%EC%BF%BC%EB%A6%AC%EB%AC%B8%EC%A0%9C
- https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85
- https://ict-nroo.tistory.com/132
Index와 성능
인덱스는 컬럼 값과 행 위치를 정렬된 구조(InnoDB는 B+Tree)로 따로 저장해서, 전체 테이블을 훑지 않고 원하는 범위만 찾게 해 준다. 대가로 쓰기 때마다 인덱스도 갱신해야 하고 저장 공간이 든다. 성능에 영향을 주는 포인트는 다음과 같다.
- 복합 인덱스는 왼쪽 컬럼부터 순서대로 쓰인다.
(a, b)인덱스는WHERE b = ?만으로는 효율적으로 쓰이지 않는다. - 인덱스 컬럼을 함수나 연산으로 가공하면(
WHERE DATE(created_at) = ...) 인덱스 범위 탐색을 하지 못한다. - 조회에 필요한 컬럼이 모두 인덱스에 있으면(커버링 인덱스) 테이블 행을 다시 읽지 않아 빠르다.
- 조건에 맞는 행이 전체의 큰 비율이면 옵티마이저는 인덱스보다 풀 스캔을 고를 수 있다.
어떤 인덱스가 실제로 쓰이는지는 EXPLAIN으로 확인한다.
- https://ddotle99.tistory.com/3
- https://jojoldu.tistory.com/243
- https://tecoble.techcourse.co.kr/post/2021-09-18-db-index/
- https://dataonair.or.kr/db-tech-reference/d-story/data-story/?mod=document&uid=63127
Boolean Index
boolean 컬럼처럼 값 종류가 두세 개뿐인 컬럼은 카디널리티가 낮아서, 인덱스로 고른 행이 전체의 절반 가까이 되면 인덱스를 타고 행을 하나씩 찾는 것보다 풀 스캔이 낫다. 그래서 boolean 단독 인덱스는 대개 효과가 없다. 예외는 분포가 크게 치우친 경우다. 전체의 극히 일부만 true이고 그 일부를 자주 조회한다면 인덱스가 도움이 된다. 다른 조건과 함께 쓰는 복합 인덱스의 한 컬럼으로 넣는 것도 방법이다.
- https://stackoverflow.com/questions/10524651/is-there-any-performance-gain-in-indexing-a-boolean-field
하나의 Controller, 여러 개의 Thread
Spring의 컨트롤러는 기본적으로 싱글톤 빈이라 인스턴스가 하나뿐이다. 요청은 Tomcat 스레드 풀의 여러 스레드가 동시에 처리하므로, 같은 컨트롤러 객체의 메서드를 여러 스레드가 동시에 실행한다. 메서드의 지역 변수는 스레드마다 스택에 따로 생기므로 문제가 없지만, 컨트롤러나 서비스의 인스턴스 필드에 요청별 상태를 저장하면 스레드끼리 값을 덮어쓴다. 그래서 싱글톤 빈은 상태를 갖지 않게(stateless) 만들어야 한다.
- https://jeong-pro.tistory.com/204
JVM 메모리
JVM 메모리는 크게 힙과 비힙으로 나뉜다. 힙에는 애플리케이션이 만든 객체가 저장되고 GC 대상이며 부하에 따라 사용량이 변한다. 비힙에는 클래스 메타데이터(Metaspace), JIT 컴파일 결과를 담는 코드 캐시, 스레드 스택, GC 자체의 자료구조 등이 있고, 애플리케이션이 안정되면 크기가 비교적 일정하다.
Spring 블로그 글이 짚는 실무 포인트는 컨테이너 메모리 설정이다. 컨테이너 메모리 한도를 늘리면 늘어난 만큼은 주로 힙에 써야 하는데, JVM이 기본 비율로 나누면 비힙에 과하게, 힙에 부족하게 배분될 수 있다고 한다. 그래서 -Xmx만 볼 것이 아니라 스레드 수와 클래스 수에서 비롯되는 비힙 사용량까지 고려해 컨테이너 크기를 정해야 한다(Spring Blog: Memory Footprint of the JVM).
- https://velog.io/@litien/JVM-%EA%B5%AC%EC%A1%B0
- https://spring.io/blog/2019/03/11/memory-footprint-of-the-jvm#:~:text=The%20JVM%20divides%20its%20memory,referenced%20and%20are%20garbage%20collected.
추상 클래스 vs 인터페이스
추상 클래스는 상태(필드)와 생성자를 가질 수 있고 단일 상속만 된다. 공통 구현을 물려주는 “is-a” 관계에 쓴다. 인터페이스는 상태를 갖지 않고 여러 개를 구현할 수 있으며, 구현 클래스가 지켜야 할 역할(계약)을 정의한다. Java 8부터 인터페이스도 default 메서드로 구현을 가질 수 있게 되어 경계가 흐려졌지만, 인스턴스 필드를 가질 수 없다는 차이는 그대로다. 그래서 타입은 인터페이스로 정의하고, 공통 구현이 필요하면 그 인터페이스를 구현한 추상 클래스를 골격으로 두는 방식이 자주 쓰인다.
- https://brunch.co.kr/@kd4/6
댓글
아직 댓글이 없습니다