🤖

【初心者向け 100】SpringBoot核心原理

2023/10/14に公開

Spring

この記事は、こちらの講座からまとめた内容をまとめております。

https://www.inflearn.com/course/스프링-핵심-원리-기본편/dashboard

はじめに

https://zenn.dev/eldorado215/articles/d42519d4d9ede7

前回、SpringBoot入門講座を通して、とても広くて浅くspringを勉強してみました。
それでは、なぜSpringを利用し、何のためにSpringを使うのか?
その起源について簡単に紹介したいと思います。

昔、古代のエンジニアはEJBというJava標準技術でサーバーを開発しました。
様々な機能を具象できるものでしたが、問題点がありました。

遅い、EJBに合わせて設計しなければならないことでOOPが具象することが難しい。
そのため、エラーが生じたり、メインテナンス、テストなども難しい技術でしたので、
エンジニアの間ではPOJO(純粋なJavaのオブジェクトを活用しようとする動き)に戻ろうという動きもあったようです。

この不便な技術を批判し、あるエンジニアは帰宅後、時間を作り30000ラインのコードを作成しながら、EJBを代替できる仕組みを書籍で出版します。

Rod Johnsonにより、公開されたこのコードがSprngのオリジンです。
EJBという冬から春がきたという意味でSpringと名づけ、SpringはJavaのOOP を具象できるツールを目的としてオープンソースとして公開されます。

いいオブジェクトとは?

https://zenn.dev/eldorado215/articles/ede6d37dd252ec#ocp%2C-open-closed-principle%2C開放%2F閉鎖原則

以前、SOLIDの一つ、OCPについて勉強したことがあります。
拡張に解放的で、修正については閉鎖的。
要するに、既存のコードを維持しつつ、機能の拡張、入れ替えが簡単にできる。

これがポイントで、これはポリモーフィズムと繋がります。

まず、一番大事なところは世の中を役割(インターフェース)と具象体(クラス)に区切ることが大事です。

画像から運転手はプログラミングではクライアント になり、運転手というインターフェースでもあります。
この運転手はただ運転をする人です。
そして、運転手は**自動車という役割のインターフェースに依存します。

ここで、重要なポインは自動車という役割を具象したクラスたち(honda, toyota, tesla)が変わっても運転手はただ運転をすればオッケーです。 いきなり、車が変わって、飛行機の資格が必要になったりする場合はないように、クライアント側には一切の影響を与えません。

つまり、コードを修正したり、することがよくないことです。

美容室が気にいらなくて、他の美容室にいっても私たちは髪を切れば終わりです。
新しいゲームがしたい場合は、新しいゲームのbluelayを入れ替えたら終わりです。

世の中はこのようなポリモーフィズムに溢れており、OOPもこのような単純で柔軟なプログラミングを目指しています。

IoC・DI container

クライアント側(使用する側)のクラスはインターフェースに依存することで、DIは成り立てます。
しかし、今まで我々はnew演算子でいつも必要なクラスのオブジェクトを生成してきました。
なぜでしょうか?そうしなきゃクラス、具象体がないので、Nullになり参照するオブジェクトがないからです。

このようにOOTでプログラマーがコードの制御権を握っていますが、
必要なオブジェクトを生成したり、配置する流れをFrameworkに任せること、つまり制御権を逆転されたことをIoC (制御の逆転) といいます。

そして、任せたいオブジェクトを込めるところをIoC・DI Containerといいます。

先日、作成した@ConfigurationDI Containerです。
@BeanはDI Containerではなく、まずSpring Containerに登録されます。

Springの場合、Javaのresourceにあるmetaフォルダに作成するconfig関連xmlになります。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){
        return  new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository(){
        return  new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(),discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy(){
       // return  new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}

Spring container

 ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        applicationContext.getBean("memberService",MemberService.class);

先日、Springで勉強したことがありますが、こちらが Spring Container でした。

ApplicationContxtの中に、@ConfigurationにマッピングしたAnnotationConfigApplicationContextを入れ、(AppConfig.class)で動的にオブジェクトからクラスの情報を読み取るreflectionを活用します。

01:32:54.524 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
01:32:54.537 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberService'
01:32:54.578 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberRepository'
01:32:54.584 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
01:32:54.588 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'discountPolicy'

これにより、先ほどのDI container(AppConfig)の中にあったBeanが登録されます。

Spring Bean 紹介

ApplicationContextに登録したBeanを探すためにはgetBeanメソッドで探すことができることが分かりました。

Springは.classのようにクラスのタイプで探す方法と、Spring beanのname(memberService<-@Beanのメソッド名) で探す方法があります。

タイプで探す方法が一見、簡単には見えますが、同じクラスタイプで探す場合はStringContainerからどのクラスのオブジェクトをDIするか判断できないので、エラーが生じます。

テストコード
public class ApplicationContextSameBeanFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("同じタイプのbeansを二つ以上ならエラー")
    void findBeanByTypeDuplicate(){
        ac.getBean(MemberRepository.class);
    }

    @Configuration
    static class SameBeanConfig{
        @Bean
        public MemberRepository memberRepository1(){
            return new MemoryMemberRepository();
        }

        @Bean
        public MemberRepository memberRepository2(){
            return new MemoryMemberRepository();
        }
    }

}

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.member.MemberRepository' available: expected single matching bean but found 2: memberRepository1,memberRepository2

このような状況を避けるためには、メソッド名(=SpringBean名)で探せば解決することが可能です。

もしくはgetBeansOfType()というメソッドでMapをもらうことも可能です。

 @Test
    @DisplayName("findAllBeansByType")
    void findAllBeansByType() {
        Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + "value = " + beansOfType.get(key));
            assertThat(beansOfType.size()).isEqualTo(2);
        }

    }

Spring Singleton DI Container(=Spring Container)

WEBの場合、つねにRequestがあるので、いちしち新しいオブジェクトを生成するのはメモリ効率に悪いです。1秒に50000tps(1秒で50000個のオブジェクトを生成)になる場合もあるようです!
Springは基本的にSingletonでオブジェクトを生成し、メモリを節約します。
また、JavaからのSingletonが持っている限界をフォローします。
例えば、javaのこのようなコードがあるとしましょう

public class SingletonService {
    
    private static final SingletonService instance = new SingletonService();
 
    public static SingletonService getInstance() {
        return instance;
    }

    private SingletonService() {
    }
    public void logic() {
        System.out.println("singleton");
    }
}

このSingletonServiceを使うには外部からSingletonService.getInstance()というメソッドを呼び出す必要があります。SingletonServiceは具体的なクラスになっており、具体的なものに依存してはいけないというDIPを違反します。

しかし、ApplicationContextというSpring Containerが自分でSingletonでオブジェクトを生成しますで、色々な問題を解決することができます。

超超超重要★Singleton パターンの注意点★

とても重要なので、3回復唱しましょう。

SingleTonはとても便利ですが、一つのオブジェクトをシェアするので、状態があれば、とんてもない大きいな問題が起きます。

package hello.core.singleton;
public class StatefulService {

 private int price; //<=絶対絶対絶対ダメ
 
 public void order(String name, int price) {
 System.out.println("name = " + name + " price = " + price);
 this.price = price; //<=首になるので、絶対絶対絶対ダメ
 }
 public int getPrice() {
 return price;
 }
}

万が一あるサービスオブジェクトをSingletonに生成し、シェアし、price, priceを変更するメソッドがあると想像してみます。
そして、UserAが10000円を注文し、UserBが20000円を注文したと想像しましょう。

class StatefulServiceTest {

    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new
                AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean("statefulService",
                StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService",
                StatefulService.class);
        //ThreadA: user A 10000円注文
        statefulService1.order("userA", 10000);
        //ThreadB:  user B 10000円注文
        statefulService2.order("userB", 20000);
        //ThreadA: user Aの注文紹介
        int price = statefulService1.getPrice();
        System.out.println("user A price = " + price);
        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }
    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}
user A price = 20000

最後にオブジェクトを状態があり、そのpriceという状態を修正するメソッドがThread BからOverwriteになったので、priceは20000円になる、絶対ユーザーのためにはあってはいけない状況が起こります!

package hello.core.singleton;
public class StatefulService {
 
 public void order(String name, int price) {
 System.out.println("name = " + name + " price = " + price);
 return price;
 }

}

このように共有されるフィールドを宣言せず、メソッド内からロカール変数で処理します!

Component Scan

@componentを付けたクラスを自動的にspring beanに登録
(@Service, @Repository, @Configurationは内部に@componentがあるので、自動登録)

excludeFilter

@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, classes = Configuration.class)
)

このように excludeFiltersで特定の@Componentを除外することもできます。
例)@Configuration.class ー>@Configurationがつけたクラスは自動登録対象から除外

Spring bean

登録するSpring bean名はクラスの大文字ではなく、小文字に変換され、Spring beanに登録します。

MemberService => memberService

@Component("MemberService")で自分で名前を登録することも可能ですが、まずはルールを従ったほうがいいと思います。

しかし、実務では@ComponentScanをつくことは少ないようです。
なぜなら、最上位パッケージにあるCoreApplicationの@SpringBootApplicationというアノテーションにすでに@ComponentScanが存在しているからです。

@SpringBootApplication
public class CoreApplication {

	public static void main(String[] args) {
		SpringApplication.run(CoreApplication.class, args);
	}

}

そのため、@ConfigurationのようにSingtonを保証するアノテーションも最上位ディレクトリに配置することが普通です。

Discussion