내가 개발해볼게!!

원티드 프리온보딩 백엔드 챌린지 7월 | Week 1-2 사용자 수에 따른 규모를 확장하는 방법 1편 본문

외부활동

원티드 프리온보딩 백엔드 챌린지 7월 | Week 1-2 사용자 수에 따른 규모를 확장하는 방법 1편

보송송희 2023. 7. 19. 21:06

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/

 

Spring 레플리케이션 트랜잭션 처리 방식 - Yun Blog | 기술 블로그

Spring 레플리케이션 트랜잭션 처리 방식 - Yun Blog | 기술 블로그

cheese10yun.github.io

http://webcache.googleusercontent.com/search?q=cache:Lw-c3SLW8LEJ:kwon37xi.egloos.com/5364167&cd=11&hl=ko&ct=clnk&gl=kr 

 

 

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에 접속한다

  • 장점
    • 서버 동기화를 걱정할 필요 없다
  • 단점
    • 네트워크 호출이 필요하고, 때문에 상대적으로 로컬 캐시보다 느리다
    • 캐시 서버에 장애가 생기면 아무것도 할 수 없기 때문에 이에 대한 대비가 필요하다

 

 

https://dev.gmarket.com/16

 

Java Application 성능개선에 대해 알아보자 - Local Cache 편

시작하기 앞서... Application을 개발하다 보면 기능이 점점 복잡해지고 데이터가 쌓이면서, 처음과 다른 성능 저하가 발생하게 된다. 서버의 사양을 올리거나, 서버의 댓수를 추가하면 전체적인 성

dev.gmarket.com

읽어볼 만한 글. EhCache를 사용해 로컬 캐시를 적용함으로써 물리적인 증설 없이도 Applicatioin적인 측면에서 성능을 개선했다고 한다..!

 

3) CDN (Contents Delivery Network)

3-1 CDN이란?

https://www.akamai.com/ko/glossary/what-is-a-cdn

 

콘텐츠 전송 네트워크(CDN)란? - CDN 솔루션 | Akamai

콘텐츠 전송 네트워크(CDN)는 사용자와 가까운 곳에서 콘텐츠를 전송함으로써 더 빠르고 안정적인 온라인 경험을 제공합니다. 자세히 알아보세요.

www.akamai.com

지리적으로 분산된 여러 개의 서버. 정적 콘텐츠를 사용자의 물리적 위치와 가까운 프록시 서버에 캐싱함으로써 콘텐츠 로딩을 기다릴 필요가 없어진다. 

 

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

 

[MySQL] MVCC(다중 버전 동시성 제어)와 데이터베이스가 트랜잭션을 지원하는 방법과 동작 과정

이번에는 데이터베이스가 트랜잭션을 지원하는 방법과 동작 과정에 대해 살펴보도록 하겠습니다. 아래의 내용은 RealMySQL과 MySQL 공식 문서 등을 참고하여 작성한 내용입니다. 1. MVCC(다중 버전 동

mangkyu.tistory.com

 

지속성

  • 트랜잭션이 성공적으로 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월 | 원티드

무료로 양질의 교육을 들어보세요! 챌린저만을 위한 다양한 혜택을 제공해드리고 있습니다.

www.wanted.co.kr