내가 개발해볼게!!
원티드 프리온보딩 백엔드 챌린지 7월 | Week 1-2 사용자 수에 따른 규모를 확장하는 방법 1편 본문
Session 1. 사용자 수에 따른 규모를 확장하는 방법
1) 데이터베이스 다중화
1-1 목적
대부분의 서비스에서는 쓰기 연산보다 읽기 연산이 훨씬 많이 일어난다. 따라서 데이터 변경(쓰기 연산)은 주 데이터베이스로, 읽기 연산은 부 데이터베이스 서버로 분산해주면 성능이 좋아진다.
1-2 개념
주 데이터베이스(Master) 한 대와 부 데이터베이스(Slave) 여러 대로 DB 구성
- 주 데이터베이스(Master)
- 쓰기 연산(Insert, Update, Delete)을 지원한다
- 부 데이터베이스(Slave)
- 주 데이터베이스로부터 DB의 사본을 전달받는다
- 읽기 연산(Select)을 지원한다
- 트래픽이 커지면 사용상 문제가 생기는데, 이럴 때 CQRS를 사용한다
1-3 스프링에서는 어떻게 할까?
하나의 데이터소스를 사용할 경우 작성된 설정파일을 바탕으로 스프링에서 자동으로 데이터소스를 생성해준다.
하지만 두 개 이상의 데이터소스를 사용하는 경우 스프링에서 자동으로 데이터소스를 생성해주지 않기 때문에 추가적인 코드가 필요하다.
spring:
datasource:
master:
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: ...
read-only: false
username: root
password: 1234
slave:
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: ...
read-only: true
username: root
password:1234
두 개 이상의 데이터소스를 사용할 때의 yaml 파일. 여기서 hikari는 성능이 좀 더 좋은 connection pool이다
두 개 이상의 데이터소스를 사용할 때 어떤 추가적인 코드가 필요할까?
① 등록한 데이터소스에 대한 Bean을 수동으로 등록
@Configuration
public class MasterDataConfig{
@Primary
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix="spring.datasource.master.hikari")
public DataSource masterDataSource(){
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
}
@Configuration
public class SlaveDataConfig{
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix="spring.datasource.slave.hikari")
public DataSource slaveDataSource(){
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
}
② 스프링의 트랜잭션 readOnly 옵션에 따른 분기 처리
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource{
@Override
protected Object determineCurrentLookupKey(){
DataSourceType dataSourceType = TransactionSynchronizationManager
.isCurrentTransactionReadOnly() ? DataSourceType.Slave : DataSourceType.Master;
return dataSourceType;
}
}
③ 추가적인 설정
https://cheese10yun.github.io/spring-transaction/
+
AWS Aurora MySQL을 사용하면서 MariaDB Connector / J를 사용하면 master 데이터소스 하나만 등록하고 읽기 트랜잭션만 명시해주면 복잡한 코드 작성 없이도 자동으로 요청 분기가 처리된다.
이는 최신 버전의 MariaDB Connector / J에서는 지원되지 않지만 현재로서는 대안이 없기 때문에 현업에서도 해당 드라이버를 사용하고 있다고 한다 ..
2) 캐시
2-1 캐시란?
비용이 많이 드는 연산 결과나 자주 참조되는 데이터를 미리 담아두고 사용하도록 하는 저장소
장점: DB를 공유하지 않아도 돼서 DB 부하를 줄일 수 있다. 서비스에는 무조건 있는 게 이득이다!
만약 동일한 결과를 반복적으로 돌려주는 API가 있다고 생각하자. (예: 공지사항, 자주 하는 질문, ...)
해당 API는 요청을 받을 때마다 Controller -> Service -> Repository를 거친 다음 DB 조회 및 로직을 처리하는 일련의 동일한 과정을 반복적으로 진행하기 때문에 비효율적이다.
이럴 때 캐시를 사용한다면 첫 번째 요청 이후부터는 캐시에 저장된 데이터를 바로 읽어서 전달하면 되기 때문에 Repository까지 거치지 않아도 되고, 시스템 부하를 줄일 수 있다.
2-2 로컬 캐시
각각의 WAS가 각자의 Local Cache를 가지고 있다
- 장점
- 네트워크 호출이 필요없어 이에 대한 비용이 들지 않는다
- 서버의 물리 메모리에 직접 접근하기 때문에 속도가 빠르다
- 단점
- 서버가 여러 대인 경우 동기화가 안될 수 있다
- 물리 메모리 사이즈 제약이 있기 때문에 무한정 캐싱하기 어렵다
2-3 글로벌 캐시
WAS들이 네트워크를 통해 한 대의 Cache Server에 접속한다
- 장점
- 서버 동기화를 걱정할 필요 없다
- 단점
- 네트워크 호출이 필요하고, 때문에 상대적으로 로컬 캐시보다 느리다
- 캐시 서버에 장애가 생기면 아무것도 할 수 없기 때문에 이에 대한 대비가 필요하다
읽어볼 만한 글. EhCache를 사용해 로컬 캐시를 적용함으로써 물리적인 증설 없이도 Applicatioin적인 측면에서 성능을 개선했다고 한다..!
3) CDN (Contents Delivery Network)
3-1 CDN이란?
https://www.akamai.com/ko/glossary/what-is-a-cdn
지리적으로 분산된 여러 개의 서버. 정적 콘텐츠를 사용자의 물리적 위치와 가까운 프록시 서버에 캐싱함으로써 콘텐츠 로딩을 기다릴 필요가 없어진다.
3-2 사용 사례
특정 사이트에서 개발자 도구로 이미지의 URL을 확인해보면, 해당 사이트가 자체 CDN을 사용하고 있음을 알 수 있다.
4) 트랜잭션
4-1 개념
애플리케이션에서 데이터를 읽고 쓰는 과정들을 하나의 논리적 단위로 묶는 방법
한 트랜잭션 내의 모든 읽기와 쓰기는 한 연산으로 실행되고, 전체가 성공(commit)하거나 실패(rollback)한다.
4-2 ACID의 의미
트랜잭션이 보장하는 안정성. 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)의 약어
원자성, 격리성, 지속성은 데이터베이스의 속성이고, 일관성은 애플리케이션의 속성에 해당한다.
원자성
- 한 트랜잭션은 모두 Commit되거나, Rollback되어야 한다
- 이때 비즈니스 로직 실패, 네트워크 단절 등의 이유로 rollback이 발생한다
일관성
- 데이터 불변식을 보장한다(예: 회계 시스템에서 모든 계좌에 걸친 대변과 차변은 항상 맞아 떨어져야 한다)
- 이때 데이터베이스는 불변식을 위반하는 잘못된 데이터를 쓰지 못하게 막을 수 없고, 애플리케이션 측에서 데이터가 유효한지 아닌지 정의해야 한다. 즉 일관성은 데이터베이스의 속성이 아닌 애플리케이션의 속성이다
격리성
- 동시에 실행되는 트랜잭션은 서로 격리된다
- 여러 곳에서 동일한 데이터베이스의 동일 레코드에 접근하면 동시성 문제가 발생하기 때문에 적절한 전략을 선택해야 한다.
- 직렬성 격리(Serializable isolation)를 사용하면 전체 DB에서 실행되는 유일한 트랜잭션인 것처럼 동작할 수 있지만, 성능이 떨어져 실제 시스템에서 많이 사용되지는 않는다.
- 면접 전에 READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ에 대해 공부해보는 것을 추천해주셨다!
https://mangkyu.tistory.com/288
지속성
- 트랜잭션이 성공적으로 commit되었다면 하드웨어나 데이터베이스에 결함이 발생하더라도 데이터가 손실되지 않는다.
- 일반적으로 비휘발성 저장소(하드 디스크, SSD)에 기록되었다는 뜻
- 하지만 완벽한 지속성은 존재하지 않는다.
4-3 스프링의 @Transactional 애노테이션은 어떻게 동작할까?
5) DB Lock
낙관적 락 (Optimistic Lock)
트랜잭션이 commit될 때 격리가 위반되었는지 데이터베이스에서 체크하고, 만약 위반되었다면 해당 트랜잭션을 rollback한다.
DB 경합이 심하지 않은 상황이라면 낙관적 락이 비관적 락보다 성능이 좋다. 경쟁이 심해지면 rollback되는 비율이 높아지기 때문에 성능이 떨어진다.
d
@Entity
@OptimisticLocking(type = OptimisticLockType.VERSION)
public class Product {
...
@Version
private Long version;
}
비관적 락 (Pessimistic Lock)
각 트랜잭션이 실행되는 동안 전체 데이터베이스에 독점 잠금을 획득하고, 다른 트랜잭션은 락이 끝날 때까지 대기한다.
개별 트랜잭션의 성능을 향상시키는 방법 외에는 락 시간을 줄이기 힘들다.
최대한 안 쓰는 게 좋은 방법!
s Lock
다른 사용자가 동시에 읽을 수는 있지만, 동시 Update와 Delete를 방지한다.
JPA: PESSIMISTIC.READ
x Lock
다른 사용자의 읽기, 수정, 삭제를 불가능하게 한다
JPA: PESSIMISTIC.WRITE
분산락
여러 서버에서 공유된 데이터를 제어하기 위해 사용한다.
분산락 저장소로 Redis를 많이 사용하고, ZooKeeper를 사용해 구현할 수 있다.
Java와 Redis를 사용한다면 Redisson을 사용해 쉽게 분산락을 사용할 수 있다. (스핀락, Pub/Sub)
분산락 자체는 그렇게 로직이 복잡하지 않은데, 추상화가 많이 되어 있으면 테스트 코드 작성이 어려워진다.
(Redis: 유튜브 강의-우아한테크, 패캠 + 써보면서 공부)
아하! 모먼트 : 내가 면접을 준비했던 방법
이력서를 깔끔하게 잘 쓰는 법을 알려주셨다. 파워포인트도 좋지만 canva를 사용해볼 것
기본 개념을 확실히 하고, 내가 이걸 왜 했는지 꼭 생각해보라고 하셨다. ~를 왜 사용하는지...
😏
개인적으로 4번의 수업 중 가장 유익했던 시간이었다고 생각이 든다. 트랜잭션과 락에 대한 지식이 부족했는데, 이번 수업을 통해 많이 얻어갈 수 있었다. 어엿한 백엔드 개발자로 거듭나기 위해서 꼭 알아야 하는 부분이 트랜잭션 파트라고 생각하는데 이번 기회에 최대한 공부하고 넘어가고 싶다. 분산락 저장소인 Redis도 공부하고 싶다.
+ 스프링의 @Transactional이 어떻게 동작하는지 생각해보라고 말씀해주셨는데, 스프링 공식 문서를 읽어봐야겠다!
https://www.wanted.co.kr/events/pre_challenge_be_9
'외부활동' 카테고리의 다른 글
원티드 프리온보딩 백엔드 챌린지 7월 | Week 1-1 기술 면접에서 시스템 설계 문제가 가지는 의미 (0) | 2023.07.12 |
---|