Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: AOP 적용해 읽기/쓰기 DB로 요청 분산 #503

Merged
merged 23 commits into from
Oct 7, 2024

Conversation

ChooSeoyeon
Copy link
Member

@ChooSeoyeon ChooSeoyeon commented Sep 26, 2024

📌 관련 이슈

close #498

✨ 작업 내용

  • AOP 적용해 읽기/쓰기 DB로 요청 분산
    • WriterDatabase 어노테이션 사용 시 writerDataSource 사용
    • WriterDatabase 어노테이션 사용 안할 시 readerDataSource 사용

📚 기타

  • Datasource 설정 분리
  • WriterDatabase 어노테이션 생성
  • AOP 적용해 Datasource 동적 변경
  • open session in view 끔 (인증필터에서 사용한 datasource가 뒷단까지 전달되는 거 방지)

Copy link
Member

@masonkimseoul masonkimseoul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다.
코멘트 확인해주세요.

Comment on lines +11 to +27
@Slf4j
@Profile("prod")
@Aspect
@Component
public class DataSourceAspect {

@Before("@annotation(writerDatabase)")
public void setWriterDataSource(WriterDatabase writerDatabase) {
log.debug("DataSourceAspect - Before advice: Switching to 'write' data source");
DataSourceRouter.setDataSourceKey("write");
}

@After("@annotation(writerDatabase)")
public void setReaderDataSource(WriterDatabase writerDatabase) {
log.debug("DataSourceAspect - After advice: Switching back to 'read' data source");
DataSourceRouter.setDataSourceKey("read");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 클래스는 어떻게 작동하나요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래의 서비스가 있다고 가정해봐요 😄

@WriterDatabase
public Response save(Request request) { 
    Entity entity = new Entity(request);
    Result result = Repository.save(entity)
    return new Response(result);
}
  1. controller에서 save 메서드를 호출할 할 때 @WriterDatabase 어노테이션을 인식하게 됩니다.
  2. setWriterDataSource가 호출되서 데이터 소스가 write로 변경됩니다. 이렇게 되면 Repository.save 할 때 writer DB로 설정한 DBMS에 쿼리가 실행됩니다.
  3. save 메서드가 종료 된 후 @After 어노테이션 붙은 setReaderDataSource가 호출되어 데이터 소스가 다시 read로 변경되는 구조에요~

@Configuration
@RequiredArgsConstructor
@EnableJpaRepositories(basePackages = "com.zzang.chongdae")
public class DataSourceConfig {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 클래스도 궁금합니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

보시다 시피 prod 환경일 때만 실행되는 설정파일이고, reader db와 writer db를 설정해주는 역할을 합니다~

데이터베이스가 하나라면 spring이 자동으로 datasource를 찾아주는데, 두 개의 datasource를 사용하게 되면 dataSource를 재정의 해야한다고 합니다.

그래서 재정의해주기 위해 writerDataSource와 readDataSuource를 datasourceRouter에 등록했습니다~

제가 추측하기로는 이렇고 추가 공부가 필요해요 ㅜㅜ

@Slf4j
public class DataSourceRouter extends AbstractRoutingDataSource {

private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

쓰레드 로컬을 도입한 이유가 궁금합니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+) contextHolder는 멀티스레드 환경에서 스레드별 데이터소스를 지정하기 위한 용도가 맞나요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

각 API 요청마다 reader, writerdb 를 구분하기 위해 ThreadLocal을 도입했습니다.

멀티스레드 환경에서 스레드 단위로 변수(contextHolder)를 만들 수 있어요.

DataSourceRouter는 상태가 있는 객체이고 @Bean으로 등록되어 있기 때문에 요청을 처리하는 쓰레드들이 현재 설정되어 있는 dataSource를 공유하는 구조가 되어버립니다 😢

그래서 ThreadLocal을 활용하여 아래처럼 각 요청마다 dataSource를 설정할 수 있도록 변경했어요..!

image

jpa:
defer-datasource-initialization: false
hibernate:
ddl-auto: validate
ddl-auto: none
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 옵션을 none으로 설정한 이유가 있을까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다시 validate로 설정해도 되겠는데요? 테스트하다가 누락된 것으로 보입니다

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분은 추후 ci 스크립트 넣어서 validate 미리 사용해봐도 좋을 거 같네요~

@Profile("prod")
@Configuration
@RequiredArgsConstructor
@EnableJpaRepositories(basePackages = "com.zzang.chongdae")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EnableJapRepository는 왜 붙여지게 된건가요? 필요없지 않나요??

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필요 없는 것으로 판결

Copy link
Contributor

@helenason helenason left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드 잘 봤습니다!

Spring AOP, DB 분리 등 생소한 개념들이라 재밌게 했네요 :)

관련하여 질문이랑 제안 남겨두었으니 확인 부탁 드립니다~~~

@Slf4j
public class DataSourceRouter extends AbstractRoutingDataSource {

private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+) contextHolder는 멀티스레드 환경에서 스레드별 데이터소스를 지정하기 위한 용도가 맞나요?


@Profile("prod")
@Configuration
@RequiredArgsConstructor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Config 파일에 생성자는 왜 필요한 건가용?

@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari.read")
public DataSource readDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[질문] 위와 같이 빌더를 통해 데이터소스를 생성하는 방식과 new HikariDataSource()와 같이 생성자를 통해 직접 생성하는 방식의 차이가 궁금해요!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

큰 차이 없는 것 같습니다. 가독성 측면에서 이 방법이 더 좋을 것 같아요.

public DataSource dataSource() {
return new LazyConnectionDataSourceProxy(routeDataSource());
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[질문] LazyConnectionDataSourceProxy 클래스로 감싼 데이터소스를 사용하는 이유는 성능 때문이 맞나요? (데이터 소스에 대한 연결이 실제로 필요할 때까지 지연되어 성능 최적화에 도움)

[제안] 코드의 흐름상 메서드 구현 순서가 역순이 되면 더 자연스럽지 않을까 싶은데, 어떻게 생각하시나요? 어차피 @DependsOn을 통해 빈 초기화 순서를 지정하고 있기 때문에 주요 빈을 먼저 선언하는 게 좋을 것 같아요! (dataSource -> routeDataSource -> writeDataSource -> readDataSource)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

성능을 위해 사용하는 것 맞습니다. (LazyConnectionDataSourceProxy)
제안해주신 방안은 적용하겠습니다.

@@ -11,6 +11,7 @@ spring:
properties:
hibernate:
format_sql: true
open-in-view: false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 속성이 true로 설정되어 있을 때 어떤 문제가 발생했는지 공유해주시면 감사하겠습니다 :)

Copy link
Contributor

@fromitive fromitive Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open session in view 가 true일 경우 view 부터 트렌젝션이 시작됩니다.
그리고, Transaction 기본 전파 전략은 REQUIRED_NEW 이므로 이는, 기존 트렌젝션이 존재할 경우 새로운 트렌젝션을 새로 생성하지 않습니다 즉, Service에 Transactional을 사용해도 기존 트렌젝션을 유지합니다.
따라서 view에서 Service 메서드를 호출할 때 기존 트렌젝션을 사용하고, 해당 트렌젝션에서 사용하는 DataSource는 우리가 설정한 defaultDataSource를 사용하는데, 이 default dataSource가 readDatabase를 사용해서 Write시 writerDatabase를 사용할 수없는 문제가 발생하게 되었습니다.

다음에 그림으로 설명해 줄게용

Copy link
Contributor

@helenason helenason left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고했어요 재밌당

@ChooSeoyeon ChooSeoyeon merged commit 82fd915 into develop-BE Oct 7, 2024
@ChooSeoyeon ChooSeoyeon deleted the feature/498-transaction-readonly-new branch October 7, 2024 04:58
ChooSeoyeon added a commit that referenced this pull request Oct 11, 2024
* chore: 커스텀하게 만든 datasource 사용해 db 연결

* feat: transactional readonly 값에 따라 datasource 변경

* feat: transactional을 writerDatabase 어노테이션으로 대체

* feat: DataSource 관련 동작 prod 프로필로 제한

* feat: WriterDatabase 어노테이션 적용

* feat: writerDatasource 사용 후 readerDatasource로 변경

* feat: writer db와 user db의 권한 분리

* chore: 배포 테스트

* chore: 직렬로 deploy

* chore: cicd 범위 원래대로 수정

* chore: osiv 꺼서 인증필터가 사용한 datasource의 영향 제거

* chore: 바뀐 properties 반영

* chore: prod 배포

* chore: cicd 범위 원복

* chore: health-check 요청으로 불필요하게 찍히는 로그 제거

* chore: prod 배포

* chore: cicd 범위 원복

* chore: log 레벨 info에서 debug로 변경

* chore: prod 배포

* chore: cicd 범위 원복

* chore: db 스키마에 대한 validate 검사

* refactor: 불필요한 어노테이션 제거

* refactor: 메서드 순서 사용 순대로 변경
ChooSeoyeon added a commit that referenced this pull request Oct 11, 2024
* chore: 커스텀하게 만든 datasource 사용해 db 연결

* feat: transactional readonly 값에 따라 datasource 변경

* feat: transactional을 writerDatabase 어노테이션으로 대체

* feat: DataSource 관련 동작 prod 프로필로 제한

* feat: WriterDatabase 어노테이션 적용

* feat: writerDatasource 사용 후 readerDatasource로 변경

* feat: writer db와 user db의 권한 분리

* chore: 배포 테스트

* chore: 직렬로 deploy

* chore: cicd 범위 원래대로 수정

* chore: osiv 꺼서 인증필터가 사용한 datasource의 영향 제거

* chore: 바뀐 properties 반영

* chore: prod 배포

* chore: cicd 범위 원복

* chore: health-check 요청으로 불필요하게 찍히는 로그 제거

* chore: prod 배포

* chore: cicd 범위 원복

* chore: log 레벨 info에서 debug로 변경

* chore: prod 배포

* chore: cicd 범위 원복

* chore: db 스키마에 대한 validate 검사

* refactor: 불필요한 어노테이션 제거

* refactor: 메서드 순서 사용 순대로 변경
ChooSeoyeon added a commit that referenced this pull request Oct 11, 2024
* chore: 커스텀하게 만든 datasource 사용해 db 연결

* feat: transactional readonly 값에 따라 datasource 변경

* feat: transactional을 writerDatabase 어노테이션으로 대체

* feat: DataSource 관련 동작 prod 프로필로 제한

* feat: WriterDatabase 어노테이션 적용

* feat: writerDatasource 사용 후 readerDatasource로 변경

* feat: writer db와 user db의 권한 분리

* chore: 배포 테스트

* chore: 직렬로 deploy

* chore: cicd 범위 원래대로 수정

* chore: osiv 꺼서 인증필터가 사용한 datasource의 영향 제거

* chore: 바뀐 properties 반영

* chore: prod 배포

* chore: cicd 범위 원복

* chore: health-check 요청으로 불필요하게 찍히는 로그 제거

* chore: prod 배포

* chore: cicd 범위 원복

* chore: log 레벨 info에서 debug로 변경

* chore: prod 배포

* chore: cicd 범위 원복

* chore: db 스키마에 대한 validate 검사

* refactor: 불필요한 어노테이션 제거

* refactor: 메서드 순서 사용 순대로 변경
fromitive pushed a commit that referenced this pull request Nov 28, 2024
* chore: 커스텀하게 만든 datasource 사용해 db 연결

* feat: transactional readonly 값에 따라 datasource 변경

* feat: transactional을 writerDatabase 어노테이션으로 대체

* feat: DataSource 관련 동작 prod 프로필로 제한

* feat: WriterDatabase 어노테이션 적용

* feat: writerDatasource 사용 후 readerDatasource로 변경

* feat: writer db와 user db의 권한 분리

* chore: 배포 테스트

* chore: 직렬로 deploy

* chore: cicd 범위 원래대로 수정

* chore: osiv 꺼서 인증필터가 사용한 datasource의 영향 제거

* chore: 바뀐 properties 반영

* chore: prod 배포

* chore: cicd 범위 원복

* chore: health-check 요청으로 불필요하게 찍히는 로그 제거

* chore: prod 배포

* chore: cicd 범위 원복

* chore: log 레벨 info에서 debug로 변경

* chore: prod 배포

* chore: cicd 범위 원복

* chore: db 스키마에 대한 validate 검사

* refactor: 불필요한 어노테이션 제거

* refactor: 메서드 순서 사용 순대로 변경
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants