-
Notifications
You must be signed in to change notification settings - Fork 3
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고생하셨습니다.
코멘트 확인해주세요.
@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"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 클래스는 어떻게 작동하나요?
There was a problem hiding this comment.
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);
}
- controller에서
save
메서드를 호출할 할 때@WriterDatabase
어노테이션을 인식하게 됩니다. setWriterDataSource
가 호출되서 데이터 소스가write
로 변경됩니다. 이렇게 되면 Repository.save 할 때 writer DB로 설정한 DBMS에 쿼리가 실행됩니다.save
메서드가 종료 된 후@After
어노테이션 붙은setReaderDataSource
가 호출되어 데이터 소스가 다시read
로 변경되는 구조에요~
@Configuration | ||
@RequiredArgsConstructor | ||
@EnableJpaRepositories(basePackages = "com.zzang.chongdae") | ||
public class DataSourceConfig { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 클래스도 궁금합니다.
There was a problem hiding this comment.
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<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
쓰레드 로컬을 도입한 이유가 궁금합니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+) contextHolder는 멀티스레드 환경에서 스레드별 데이터소스를 지정하기 위한 용도가 맞나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jpa: | ||
defer-datasource-initialization: false | ||
hibernate: | ||
ddl-auto: validate | ||
ddl-auto: none |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 옵션을 none으로 설정한 이유가 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
다시 validate로 설정해도 되겠는데요? 테스트하다가 누락된 것으로 보입니다
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EnableJapRepository는 왜 붙여지게 된건가요? 필요없지 않나요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
필요 없는 것으로 판결
There was a problem hiding this 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<>(); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[질문] 위와 같이 빌더를 통해 데이터소스를 생성하는 방식과 new HikariDataSource()
와 같이 생성자를 통해 직접 생성하는 방식의 차이가 궁금해요!
There was a problem hiding this comment.
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()); | ||
} | ||
} |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 속성이 true로 설정되어 있을 때 어떤 문제가 발생했는지 공유해주시면 감사하겠습니다 :)
There was a problem hiding this comment.
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를 사용할 수없는 문제가 발생하게 되었습니다.
다음에 그림으로 설명해 줄게용
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수고했어요 재밌당
* 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: 메서드 순서 사용 순대로 변경
* 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: 메서드 순서 사용 순대로 변경
* 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: 메서드 순서 사용 순대로 변경
* 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: 메서드 순서 사용 순대로 변경
📌 관련 이슈
close #498
✨ 작업 내용
📚 기타