エキサイト株式会社メディア事業部エンジニアの佐々木です。弊社アドベントカレンダー5日目を担当させていただきます。メディア事業部では、SpringBootを用いて日々ソフトウェア開発を行っていますが、サービスが大きくなったり人が増えたりするとアーキテクチャが障害にならないまでも部分的に守られていないことが起こります。Javaの世界ではArchUnitというアーキテクチャをテストしてくれるものがあり、弊社での利用を紹介いたします。
前提
openjdk 21.0.1 2023-10-17 LTS OpenJDK Runtime Environment Corretto-21.0.1.12.1 (build 21.0.1+12-LTS) OpenJDK 64-Bit Server VM Corretto-21.0.1.12.1 (build 21.0.1+12-LTS, mixed mode, sharing) ------------------------------------------------------------ Gradle 8.5 ------------------------------------------------------------ Build time: 2023-11-29 14:08:57 UTC Revision: 28aca86a7180baa17117e0e5ba01d8ea9feca598 Kotlin: 1.9.20 Groovy: 3.0.17 Ant: Apache Ant(TM) version 1.10.13 compiled on January 4 2023 JVM: 21.0.1 (Amazon.com Inc. 21.0.1+12-LTS) OS: Mac OS X 12.5 aarch64 . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.2.0)
アプリケーションのサンプルコード
今回は、アプリケーションのアーキテクチャとしてサービス階層でトランザクションを貼るようなものだとします。要件としては、トランザクションを貼る際に接続するデータベースを決定します。デフォルトでは更新系DBを参照してますが、参照系はリードレプリDBに接続を期待します。そのアノテーションが @Transactional(readonly=true)
となります。
下記は、@Transactional
のアノテーションを貼っていないケース、@Transactional(readOnly=false)
のアノテーションを貼っているケースで両方とも失敗する
ことが期待値です。
@Service // @Transactionalのアノテーションを貼っていない class DemoServiceImpl implements DemoService { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); @Override public Object get10SecondsExpirationData() throws InterruptedException { return "expiration 10 seconds \n"; } @Override public Object get5SecondsExpirationData() throws InterruptedException { return "expiration 5 seconds \n"; } } @Service @Transactional(readOnly = false) // @Transactionalのアノテーションを貼っているのだが、readOnly=trueではない class DemoService2Impl implements DemoService { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); @Override public Object get10SecondsExpirationData() throws InterruptedException { return "expiration 10 seconds \n"; } @Override public Object get5SecondsExpirationData() throws InterruptedException { return "expiration 5 seconds \n"; } }
テストコード
class ArchitectureTest { @Test @DisplayName("@Serviceの場合には@Transactionalをつけるルール") void serviceAnnotationWithCacheableTest() { JavaClasses javaClasses = new ClassFileImporter().importPackages("jp.co.excite"); // テストをするパッケージを指定します。 ArchRule archRule = classes() .that().areAnnotatedWith(Service.class) // that() を使うと対象のクラスやメソッドを絞り込めます .should().beAnnotatedWith(Transactional.class); // should()以降でアーキテクチャがどのような状態だったら正しいかを指定できます archRule.check(javaClasses); // アーキテクチャルールで抽出したクラスをテストします。 } @Test @DisplayName("@Transactionalは、readonlyをつけるルール") void serviceAnnotationWithTransactionalHaveReadonlyProperty() { JavaClasses javaClasses = new ClassFileImporter().importPackages("jp.co.excite"); // テストをするパッケージを指定します。 ArchRule archRule = classes() .that().areAnnotatedWith(Service.class) .and().areAnnotatedWith(Transactional.class) // アーキテクチャルールが複雑な場合は、無名クラスでルールの作成できます。 .should(new ArchCondition<>("@Serviceの場合には@Transactionalをつけて、中身はreadonly=trueにするルール") { @Override public void check(JavaClass item, ConditionEvents events) { boolean valid = item.getAnnotationOfType(Transactional.class).readOnly(); if (!valid) { events.add(SimpleConditionEvent.violated(item, item.getFullName())); } } }); archRule.check(javaClasses); } }
BDDのような書き方でアーキテクチャルールを書くことが可能です。
実行してみます。
ArchitectureTest > @Transactionalは、readonlyをつけるルール FAILED java.lang.AssertionError at ArchitectureTest.java:46 ArchitectureTest > @Serviceの場合には@Transactionalをつけるルール FAILED java.lang.AssertionError at ArchitectureTest.java:26 AdventcalendarApplicationTests > contextLoads() FAILED java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180 Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:802 Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException at DependencyDescriptor.java:218
@Transactionalは、readonlyをつけるルール FAILED @Serviceの場合には@Transactionalをつけるルール FAILED
期待通りテストは失敗しています。
まとめ
アーキテクチャをドキュメントに残したり、レビューで修正するのはとても大切なことですが、すり抜けてしまうことはあります。アプリケーションとしては動作してしまうので、見つけたときは修正にコストがかかってしまったりしますので、こういうテストが自動化できるのは大変ありがたいと思います。ドキュメント化をおろそかにすることなく、継続的にArchUnitの充実も指定校と思います。
最後に
エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。
募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com