지금까지의 스프링 빈을 등록할 때는 자바 코드의 @Bean이나 XML의 <bean>을 통해서 설정 정보에 직접 등록할 빈을 나열했습니다. 실제 등록한 빈은 몇개 없었지만, 등록해야할 빈이 수백개가 된다면 일일이 등록하는 번거로움, 누락하는 문제가 생길 수 있습니다.

그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공합니다. 또 의존관계도 자동으로 주입하는 @Autowired라는 기능도 제공합니다.

 

코드로 컴포넌트 스캔과 의존관계 자동 주입을 알아보고자 합니다. 기존 AppConfig는 과거 코드와 테스트를 유지하기 위해 남겨두고 새로운 객체를 만들어서 테스트 해보도록 하겠습니다.

 

AutoAppConfig

@Configuration
// excludeFilters : 컴포넌트 스캔으로 스프링 빈에 등록할 데이터 중 뺄 것들을 미리 지정
@ComponentScan(
    excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
Configuration.class))
public class AutoAppConfig {

}

컴포넌트 스캔을 사용하려면 @ComponentScan을 설정 정보에 추가하면 됩니다. 코드를 보시면 기존의 AppConfig와 다르게 @Bean으로 등록한 클래스가 없는 것을 볼 수 있습니다.

 

참고로 @ComponentScan을 사용하면 @Configuraion이 붙은 설정 정보도 자동으로 등록되기 떄문에, AppConfig, TestConfig 등 앞서 만들어두었던 설정 정보도 함께 등록되며 실행되어 버립니다. 그래서 excludeFilters를 이용하여 @Configuration으로 등록되는 객체를 스캔 대상에서 제외시켜 주었습니다.

 

컴포넌트 스캔은 이름 그대로 @Component가 붙은 클래스를 스캔해서 스프링 빈으로 등록합니다.

 

참고로 @Configuration이 컴포넌트 스캔의 대상이 된 이유도 @Configuration 소스코드를 열어보면 @Component가 설정정보로 붙어있기 떄문입니다.

 

이제 각 클래스가 컴포넌트 스캔의 대상이 되도록 @Component를 붙여주도록 하겠습니다. 또한 의존관계를 자동으로 주입해야하기 떄문에 의존관계를 주입하는 메서드였던 생성자에 @Autowired를 붙여주었습니다. 대표적으로 두 클래스의 코드만 보여주도록 하겠습니다.

 

MemoryMemberRepository 객체에 @Component 추가

@Component
public class MemoryMemberRepository implements MemberRepository {}

 

 

MemberServiceImpl 객체에 @Component, @Autowired 추가

@Component
public class MemberServiceImpl implements MemberService {

     private final MemberRepository memberRepository;
     
     // @Autowired를 사용하면 생성자에서 여러 의존관계도 한번에 주입받을 수 있습니다.
      @Autowired
      public MemberServiceImpl(MemberRepository memberRepository) {
          this.memberRepository = memberRepository;
      }
}

 

테스트 코드

public class AutoAppConfigTest {

    @Test
    void basicScan() {
        ApplicationContext ac = new
                AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

 

출력

shared instance of singleton bean 'autoAppConfig'
shared instance of singleton bean 'rateDiscountPolicy'
shared instance of singleton bean 'memberServiceImpl'
shared instance of singleton bean 'memoryMemberRepository'
shared instance of singleton bean 'orderServiceImpl'

실제 출력되는 것을 보면 @Component의 설정정보를 추가한 객체들이 스프링 빈에 등록된 것을 볼 수 있습니다.

 

컴포넌트 스캔은 알겠지만, 자동 의존관계 주입이 어떻게 동작되는지 감이 잘 오지 않을텐데요. 그림으로 한번 알아보도록 하겠습니다.

1. @ComponentScan

  • @ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록합니다.
  • 이때, 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자롤 사용합니다. (카멜표기법)
    • 만약, 빈 이름을 직접 지정하고 싶다면 @Component("지정하고 싶은 이름")을 사용하면 됩니다.

 

2. @Auowired 의존관계 자동 주입

  • 생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입합니다.
  • 이때, 기본 조회 전략은 타입이 같은 빈을 찾아서 주입합니다.
    • getBean(MemberRepository.class)와 동일하다고 이해하면 좋을 것 같습니다.
    • 생성자에 파라미터가 많아도 다 찾아서 자동으로 주입합니다.

 

 

 탐색위치와 기본 스캔 대상

 

탐색할 패키지의 시작 위치 지정 

모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸립니다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있습니다.

@ComponentScan(
          basePackages = "hello.core",
}
  • basePackages : 탐색할 패키지의 시작 위치를 지정합니다. 이 패키지를 포함해서 해당 패키지부터 하위 패키지의 모든 객체를 탐색합니다.
    • basePackages = {"hello.core", "hello.service"} 이런 식으로 시작 위치를 여러개 지정할 수도 있습니다.
  • basePackageClasses : 지정한 클래스의 패키지를 탐색 시작 위치로 지정합니다. (예를들어, AppConfig.class를 지정했을 때 AppConfig가 있는 패키지인 hello.core부터 모든 하위패키지의 객체를 탐색합니다.)
  • 만약 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 됩니다.

 

권장하는 방법

패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것 입니다. 최근 스프링 부트도 해당 방법을 기본으로 제공한다고 합니다.

 

예를 들어서 프로젝트가 아래와 같은 구조로 되어있다면

  • hello.core
  • hello.core.service
  • hello.core.repository

hello.core를 프로젝트 시작 루트, 해당 패키지에 AppConfig와 같은 메인 설정 정보를 둔 뒤 @ComponentScan을 붙이고 basePackages는 생략하는 것 입니다.

 

참고로 스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인 @SpringBootApplication을 프로젝트 시작 루트 위에 두는 것이 관례입니다. (그리고 해당 설정안에 바로 @ComponentScan이 들어있다고합니다.)

@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}

 

 

컴포넌트 스캔 기본 대상

참고로 컴포넌트 스캔은 @Component 뿐만 아니라 아래와 같은 내용도 추가로 포함됩니다.

  • @Component : 컴포넌트 스캔에서 사용
  • @Controller : 스프링 MVC 컨트롤러에서 사용
  • @Service : 스프링 비즈니스 로직에서 사용
  • @Repository : 스프링 데이터 접근 계층에서 사용
  • @Configuration : 스프링 설정 정보에서 사용
@Component
public @interface Controller {
}
    
@Component
public @interface Service {
}

@Component
public @interface Configuration {
}

위의 소스 코드를 보면 @Component를 포함하고 있는 것을 알 수 있습니다.

 

참고로 어노테이션에는 사실 상속관계라는 것이 없습니다. 그래서 어떤 어노테이션이 특정 어노테이션을 들고 있는 것을 인식할 수 있는 것은 자바 언어가 지원하는 기능은 아니고, 스프링이 지원하는 기능입니다.

 

 

필터

  • includeFilters : 컴포넌트 스캔 대상을 추가로 지정합니다.
  • excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정합니다.

빠르게 예제로 확인해보도록 하겠습니다.

 

컴포넌트 스캔 대상을 추가할 어노테이션

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}

 

컴포넌트 스캔 대상에서 제외할 어노테이션

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}

 

컴포넌트 스캔 대상에 추가할 클래스

@MyIncludeComponent
public class BeanA {
}

 

컴포넌트 스캔 대상에서 제외할 클래스

@MyExcludeComponent
public class BeanB {
}

 

테스트 코드

public class ComponentFilterAppConfigTest {

    @Test
    void filterScan() {
        ApplicationContext ac = new
                AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);

        BeanA beanA = ac.getBean("beanA", BeanA.class);
        assertThat(beanA).isNotNull();

        // 빈이 없기떄문에 터짐
        //ac.getBean("beanB", BeanB.class);
        Assertions.assertThrows(
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("beanB", BeanB.class));
    }

    @Configuration
    @ComponentScan(
            includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes =
                    MyIncludeComponent.class),
            excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes =
                    MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig {
    }
}

 

 

FilterType은 5가지 옵션이 있습니다.

  • ANNOTATION : 기본값, 어노테이션을 인식해서 동작합니다.
    • ex) org.example.SomeAnnotation
  • ASSIGNABLE_TYPE : 지정한 타입과 자식 타입을 인식해서 동작합니다.
    • ex) org.example.SomeClass
  • ASPECTJ : AspectJ 패턴 사용
    • ex) org.example..*service+
  • REGEX : 정규 표현식
    • ex) org\.example\.Default.*
  • CUSTOM : TypeFilter이라는 인터페이스를 구현해서 처리
    • ex) org.example.MyTypeFilter

 

 

 

중복 등록과 충돌

컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까요??

 

아래의 두가지 상황이 있습니다.

  1. 자동 빈 등록 vs 자동 빈 등록
  2. 수동 빈 등록 vs 자동 빈 등록

 

자동 빈 등록 vs 자동 빈 등록

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 오류(ConflictingBeanDefinitionException)를 발생시킵니다.

 

수동 빈 등록 vs 자동 빈 등록

만약 수동 빈 등록과 자동 빈 등록에서 빈 이름이 충돌되면 어떻게 될까요?

 

@Component
public class MemoryMemberRepository implements MemberRepository {}
@Configuration
@ComponentScan(
         excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
Configuration.class)
)
public class AutoAppConfig {

     @Bean(name = "memoryMemberRepository")
     public MemberRepository memberRepository() {
         return new MemoryMemberRepository();
     }
}

이 경우 수동 빈 등록이 우선권을 가집니다. (수동 빈이 자동 빈을 오버라이딩 해버립니다.)

 

출력

Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing

수동 빈, 자동 빈이 중복되는 경우 수동빈이 자동 빈을 오버라이딩 한다는 출력이 나옵니다. (스프링 짱짱)

 

물론 자동보다 수동이 우선권을 가지는 것이 맞습니다. 하지만 현실은 개발자가 의도적으로 설정해서 이런 결과가 만ㄹ들어지기 보다는 설정들이 꼬여 이런 결과가 만들어지는 경우가 대부분이기 떄문입니다.

그러면 정말 잡기 어려운 버그가 만들어진다고 합니다. 항상 잡기 어려운 버그는 애매한 버그이기 떄문입니다.

그래서 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었습니다.

 

수동 빈 등록, 자동 빈 등록 오류 시 에러

Consider renaming one of the beans or enabling overriding by setting
spring.main.allow-bean-definition-overriding=true

 

스프링 부트에서 실행해보면 오류를 볼 수 있습니다.

 

 

 

 

 

 

출처

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

웹 애플리케이션과 싱글톤

  • 스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다고 합니다.
  • 대부분의 스프링 애플리케이션은 웹 애플리케이션입니다. 물론 웹이 아닌 애플리케이션 개발도 얼마든지 개발할 수 있습니다.
  • 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 합니다.

 

고객의 수에 따라서 만들어야 하는 문제

객체가 다른 것 확인

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();

        // 1. 조회 : 호출할 때 마다 객체를 생성
        MemberService memberService = appConfig.memberService();

        // 2. 조회 : 조회할 때 마다 객체를 생성
        MemberService memberService1 = appConfig.memberService();

        // 참조값이 다른 것을 확인
        System.out.println("memberService1 : " + memberService);
        System.out.println("memberService2 : " + memberService1);

        // memberService != memberService1
        assertThat(memberService).isNotSameAs(memberService1);
    }
    
    // 출력
    // memberService1 : hello.core.member.MemberServiceImpl@1e6a3214
    //memberService2 : hello.core.member.MemberServiceImpl@3e27aa33
  • 위의 출력을 보면 실제로 참조값이 다른 것을 확인할 수 있습니다. 
  • 클라이언트가 적은 경우는 괜찮겠지만, 클라이언트가 많은 경우 고객 요청이 1초에 100개가 나오면 초당 100개가 생성되고 소멸됨으로써 메모리 낭비가 심해집니다.
  • 이를 해결하기 위해 객체가 딱 1개만 생성되고, 공유하도록 설계하면 됩니다. 이것이 싱글톤 패턴입니다.

 

 

싱글톤 패턴

싱글톤 패턴을 알기 전 디자인 패턴에 대해 잘 모르시는 분이 있을수도 있을 것 같아 조금 설명해보고자 합니다. 디자인 패턴은 과거 개발자분들이 개발하면서 겪은 노하우들을 패턴화 시킨 것 입니다. 싱글톤 같은 경우에는 메모리 낭비를 줄이고자 과거 개발자가 만든 디자인 패턴이라고 보면 됩니다.

 

싱글톤 패턴은 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴입니다. 그렇기 떄문에 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 합니다. 인스턴스가 2개 생성되는 것을 막기위해 private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 합니다.

 

public class SingletonService {

    // 1. static 영역에 객체를 딱 1개만 생성합니다.
    private static final SingletonService instance = new SingletonService();

    // 2. public으로 열어서 객체 인스턴스가 필요하면 해당 static 메서드를 통해서만 조회하도록 합니다.
    public static SingletonService getInstance(){
        return instance;
    }

    // 3. 생성자를 private으로 선언해서 외부에서 new 키ㅜ어드를 사용한 객체 생성을 못하게 막습니다.
    private SingletonService() {}

    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}
  1.  static 영역에 객체 instance를 미리 하나 생성해서 올려둡니다.
  2. 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있습니다. (항상 같은 인스턴스를 반환합니다.)
  3. 딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private로 막아서 외부에서 New 키워드로 생성되는 것을 막아줍니다.

 

Test 코드

   @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonServiceTest() {
        //private으로 생성자를 막아두었다. 컴파일 오류가 발생한다.
        // new SingletonService();

        // 1. 조회 : 호출할 때 마다 같은 객체를 반환
        SingletonService singletonService = SingletonService.getInstance();

        // 2. 조회 : 호출할 때 마다 같은 객체를 반환
        SingletonService singletonService1 = SingletonService.getInstance();

        // 참조값이 같은 것을 확인
        System.out.println("singletonService1 : " + singletonService);
        System.out.println("singletonService2 : " + singletonService1);

        // singletonService1 == singletonService2
        assertThat(singletonService).isSameAs(singletonService1);

        singletonService1.logic();
    }
    
    // 출력    
    //singletonService1 : hello.core.singleton.SingletonService@795509d9
    //singletonService2 : hello.core.singleton.SingletonService@795509d9
    //싱글톤 객체 로직 호출
  • private로 new 키워드를 막아두었습니다.
  • 호출할 때 마다 같은 객체 인스턴스를 반환하는 것을 확인할 수 있습니다.

싱글톤 패턴을 적용하면 만들어진 객체를 공유하기 떄문에 효율적으로 사용할 수 있습니다. 하지만 싱글톤 패턴은 수 많은 문제점들을 가지고 있습니다.

 

싱글톤 패턴 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어갑니다.
  • 의존관계상 클라이언트가 구체 클래스에 의존합니다. (DIP 의존관계 역전 원칙을 위반합니다.)
  • 클라이언트가 구체 클래스에 의존해서 OCP(개발-폐쇄 원칙)를 위반할 가능성이 높습니다.
  • 테스트하기 어렵습니다. (DIP, OCP, 테스트에 관해서는 아직 공감하기 어려운 것 같습니다..)
  • 내부 속성을 변경하거나 초기화하기 어렵습니다.
  • private 생성자로 자식 클래스를 만들기 어렵습니다.
  • 결론적으로 유연성이 떨어집니다.

 

 

싱글톤 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리합니다.

지금까지 학습했던 스프링 빈이 바로 싱글톤으로 관리되는 빈입니다.

 

스프링 컨테이너

  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리합니다.
    • 이전에 설명한 컨테이너 생성 과정을 자세히 보면, 컨테이너는 객체를 하나만 생성해서 관리합니다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 합니다. 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 합니다.
  • 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 하나만 생성하여 공유할 수 있습니다.
    • 싱글톤 패턴을 위한 지저분한 코드가 없어집니다.
    • DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있습니다.

Test 코드

    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        // 1. 조회 : 호출할 때 마다 같은 객체를 반환
        MemberService memberService = ac.getBean("memberService", MemberService.class);

        // 2. 조회 : 호출할 때 마다 같은 객체를 반환
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);

        // 참조값이 같은 것을 확인
        System.out.println("memberService1 : " + memberService);
        System.out.println("memberService2 : " + memberService1);

        // memberService == memberService1
        assertThat(memberService).isSameAs(memberService1);
    }

 

싱글톤 컨테이너 적용 후

스프링 컨테이너 덕분에 고객의 요청이 올때마다 이미 만들어진 객체를 공유해서 효율적으로 재사용 합니다.

 

 

 

싱글톤 방식의 주의점

  • 싱글톤 반식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 떄문에 상태를 유지(stateful)하게 설계하면 안됩니다.
  • 무상태(stateless)로 설계해야 합니다.
    • 특정 클라이언트에 의존적인 필드가 있으면 안됩니다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안됩니다.
    • 가급적 읽기만 가능해야 합니다.
    • 필드 대신 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 합니다.
  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다고 합니다.

코드로 예시를 들어보겠습니다.

빈에 등록할 클래스

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;
    }
}

 

테스트 코드

    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        //ThreadA: A사용자 10000원 주문
        statefulService1.order("userA", 10000);

        //ThreadB: B사용자 20000원 주문
        statefulService2.order("userB", 20000);

        //ThreadA: 사용자A 주문 금액 조회
        int price = statefulService1.getPrice();

        //ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
        System.out.println("price = " + price);
        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }

    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
  
    // 출력  
    //name = userA price = 10000
    //name = userB price = 20000
    //price = 20000

실제 쓰레드가 아닌 예시로 코드를 작성해보았습니다. 결과적으로 statefulService1에 price는 10000원을 등록했지만, 20000원으로 바뀌는 현상을 볼 수 있습니다. 실무에서 이런 경우가 종종 있는데 정말 어려운 큰 문제들이 터진다고 합니다. 따라서, 공유 필드는 조심해야한다고 하고 스프링 빈은 항상 무상태(stateless)로 설계하는 습관을 들이면 좋을 것 같습니다.

 

 

 

@Configuration과 싱글톤

그런데 이상한 점이 있습니다. 아래의 AppConfig.class의 코드를 보면 orderService()를 호출할 때마다 MemoryMemberRepository, MemberServiceImpl가 new로 생성되면서 반환이 됩니다. 이러면 객체를 한 번만 생성한 뒤 재사용하는 것이 아니라 계속해서 생성하는 것이 아닐까??라는 생각이 들게 됩니다. 

@Configuration          // 설정 정보에 하는 어노테이션
public class AppConfig {

    @Bean           // 각 메서드에 Bean 어노테이션을 추가하면 스프링 컨테이너에 추가됨.
    public MemberService memberService() {
        return new MemberServiceImpl(getMemberRepository());
    }

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

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(
                getMemberRepository(),
                getDigcountPolicy());
    }

    @Bean
    public DiscountPolicy getDigcountPolicy() {
        //return new FIxDiscountPolicy();
        return new RateDiscountPolicy();
    }
}


그래서 스프링 컨테이너는 이 문제를 어떻게 해결하나 직접 테스트 해보기로 합니다.

 

테스트를 위한 코드 추가

public class MemberServiceImpl implements MemberService {
      private final MemberRepository memberRepository;

      //테스트 용도
      public MemberRepository getMemberRepository() {
          return memberRepository;
      }
  }

public class OrderServiceImpl implements OrderService {
      private final MemberRepository memberRepository;

      //테스트 용도
      public MemberRepository getMemberRepository() {
          return memberRepository;
      }
}

테스트를 위해 MemberRepository를 조회할 수 있는 기능을 추가했습니다.

 

테스트 코드

    @Test
    void configurationTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        //모두 같은 인스턴스를 참고하고 있다.
        System.out.println("memberService -> memberRepository = " + memberService.getMemberRepository());
        System.out.println("orderService -> memberRepository  = " + orderService.getMemberRepository());
        System.out.println("memberRepository = " + memberRepository);

        //모두 같은 인스턴스를 참고하고 있다.
        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
    
    //출력
    //memberService -> memberRepository = hello.core.member.MemoryMemberRepository@650eab8
    //orderService -> memberRepository  = hello.core.member.MemoryMemberRepository@650eab8
    //memberRepository = hello.core.member.MemoryMemberRepository@650eab8

실제 확인해보니 다른 인스턴스가 아닌 같은 인스턴스를 재사용하는 것을 볼 수 있습니다. 실험을 통해서 어떻게 된 일인지 확인해보도록 하겠습니다.

 

AppConfig에 호출 로그를 남기는 방식을 통해 확인해보겠습니다.

    @Bean           
    public MemberService memberService() {
        //1번
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

실제 빈 메서드 마다 안에 출력 로그를 작성하였습니다. 그 후 똑같이 위에 테스트 코드를 실행시켜보았습니다.

 

출력 결과

call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService

memberRepository()는 3번 호출되어야 할 것 같지만 실제로 한번만 호출이 되는 것을 볼 수 있습니다. 어떻게 이런 일이 가능한 걸까요??

 

 

@Configuration과 바이트코드 조작의 마법

앞서 실험해보았던 memberRepository()가 1번만 호출되는 것은 @Configuraion을 적용한 AppConfig에 있습니다.

 

테스트 코드

    @Test
    void configurationDeep() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        AppConfig bean = ac.getBean(AppConfig.class);

        System.out.println("bean = " + bean.getClass());
    }
    // 출력
    // bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$b9807240
  • 사실 AnnotaionConfigApplicationContext에 파라미터로 넘긴 값은 스프링 빈으로 동록됩니다. 그래서 AppConfig도 스프링 빈이 됩니다.
  • AppConfig를 스프링 빈으로 조회해서 클래스 정보를 출력해보았습니다.

순수한 자바 클래스라면  class hello.core.AppConfig라고 출력 되야하지만, 뒤에 이상한 값이 붙습니다. 이것은 내가 만든 클래스가 아니라 스프링이 빈을 등록하는 과정속에서 조작을 하기 떄문입니다. 실제 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것 입니다.

위의 그림을 보시면, AppCOnfig@CGLIB라는 다른 클래스가 싱글톤을 보장해주도록 합니다.

 

AppConfig@CGLIB 예상 코드

@Bean
public MemberRepository memberRepository() {

    if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) { 
        return 스프링 컨테이너에서 찾아서 반환;
    } else { //스프링 컨테이너에 없으면
    기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 
        return 반환
    } 
}

@Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성하여 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어질 것이라고 에상됩니다. 덕분에 앞서서 본 memberRepository()가 3번 생성되어야 하지만, 한번만 생성되도록 즉, 싱글톤이 보장되도록 하는 것 입니다.

 

 

그렇다면?? @Configuration을 적용하지 않고, @Bean만 적용하면 어떻게 될까요??

실제 @Configuration을 삭제하고 똑같이 테스트 코드를 실행해보니

 

출력

call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
call AppConfig.memberRepository

memberRepository가 총 3번 호출되는 것을 알 수 있습니다.

 

인스턴스가 같은지 테스트 결과

memberService -> memberRepository = hello.core.member.MemoryMemberRepository@6239aba6
orderService -> memberRepository  = hello.core.member.MemoryMemberRepository@3e6104fc
memberRepository = hello.core.member.MemoryMemberRepository@12359a82

당연히 각각 다른 MemoryMemberRepository 인스턴스를 가지고 있었습니다.

 

 

정리

  • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 확실하게 보장하지 않습니다.
    • memberRepository()처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않습니다.
  • 결론적으로 스프링 설정 정보는 항상 @Configuration을 사용하면 됩니다.

 

 

 

 

출처

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

IoC, Di, 그리고 컨테이너

스프링에 대해 조금이라도 공부하면 마주치게되는 용어들입니다. 처음보면 잘 이해되지 않아 이번기회에 정확하게 이해하고 넘어가기위해 정리하고자 합니다.

 

제어의 역전 IoC (Inversion of Control)

  • 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성, 연결, 실행 했습니다. 한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했습니다. 개발자 입장에서는 본인이 작성한 코드이기때문에 자연스러운 흐름입니다.
  • 반면에 AppConfig가 등장한 이후에는 구현 객체는 자신의 로직을 실행하는 역할만 담당합니다. 프로그램의 제어 흐름은 이제 AppConfig가 가져가는 것 입니다. 예를들어 OrderServiceImpl은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모릅니다.
  • 즉, 프로그램에 대한 제어 흐름의 권한은 모두 AppConfig가 가지고 있습니다. 
  • 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라고 합니다.

 

프레임워크 vs 라이브러리

  • 프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크 입니다. (JUnit)
  • 반면에, 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아닌 라이브러리입니다.

 

의존관계 주입 DI (Dependency Injection)

  • OrderServiceImpl은 DiscountPolicy 인터페이스에 의존합니다. OrderServiceImpl은 실제 어떤 구현 객체가 사용될지는 모릅니다.
  • 의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 합니다.
  • 정적인 클래스 의존관계

 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있습니다. 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석할 수 있습니다. OrderServiceImpl은 MemberRepository, DiscountPolicy에 의존한다는 것을 알 수 있습니다. 그런데 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 OrderSrviceImpl에 주입되는 지 알 수 없습니다.

 

  • 동적인 객체 인스턴스 의존 관계

애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계입니다.

  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라고 합니다.
  • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있습니다.
  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있습니다.

 

IoC 컨테이너, DI 컨테이너

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 합니다.
  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라고 합니다.

 

 

스프링으로 전환하기

지금까지 순수한 자바 코드만으로 DI를 적용했습니다. 이제 스프링을 사용하여 프로젝트를 변경해보겠습니다.

 

  • AppConfig
package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FIxDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration          // 설정 정보에 하는 어노테이션
public class AppConfig {

    @Bean           // 각 메서드에 Bean 어노테이션을 추가하면 스프링 컨테이너에 추가됨.
    public MemberService memberService() {
        return new MemberServiceImpl(getMemberRepository());
    }

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

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(
                getMemberRepository(),
                getDigcountPolicy());
    }

    @Bean
    public DiscountPolicy getDigcountPolicy() {
        //return new FIxDiscountPolicy();
        return new RateDiscountPolicy();
    }
}

 

    • MemberApp
package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MemberApp {
    public static void main(String[] args) {
        //AppConfig appConfig = new AppConfig();
        //MemberService memberService = appConfig.memberService();

        // AppConfig 클래스에 있는 생선한 객체를 컨테이너에 넣어주고 관리해줌
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);

        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("member = " + member.getName());
        System.out.println("findmember = " + findMember.getName());
    }
}

AppConfig인 클래스에 Configuration 어노테이션을 등록하고 각 메서드마다 Bean어노테이션을 해주면 각 메서드는 스프링 컨테이너에 등록된다고 합니다. 실제로 MemberApp에서 AnnotationConfigApplicationContext 파라미터에 AppConfig.class를 넣어준 뒤 실행한 결과를 보면 아래 사진과 같이 문제없이 돌아가는 것을 볼 수 있고 로그를 보면 각 메서드명으로 스프링 컨테이너에 등록된 것을 볼 수 있습니다.

 

스프링 컨테이너

  • 위에서 사용한 ApplicationContext를 스프링 컨테이너라고 합니다.
  • 기존에는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용합니다.
  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용합니다. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록합니다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 합니다.
  • 이전에는 개발자가 필요한 객체를 AppConfig를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 합니다. 스프링 빈은 ApplicationContext.getBean() 메서드를 사용해서 찾을 수 있습니다.
  • 코드가 약간 더 복잡해진 것 같은데, 스프링 컨테이너를 사용하면 어떤 장점이 있는지 궁금해집니다. 결과부터 말해주셨는데 어마어마한 장점이 있다고 합니다.(궁금합니다..)  

 

 

스프링 컨테이너 생성

스프링 컨테이너가 생성되는 과정을 알아봅시다

// 스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
  • ApplicationContext를 스프링 컨테이너라고 합니다.
  • ApplicationContext는 인터페이스입니다. (다형성)
  • 스프링 컨테이너는 XML을 기반으로 만들 수 있고, 어노테이션 기반의 자바 설정 클래스로 만들 수 있습니다.
  • 직전에 AppConfig를 사용했던 방식이 어노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것 입니다.

 

1. 스프링 컨테이너 생성

  • new AnnotationConfigApplicationContext(AppConfig.class)
  • 스프링 컨테이너를 생성할 때는 구성 정보를 지정해주어야 합니다.
  • 여기서는 AppConfig.class를 구성 정보로 지정했습니다.

 

2. 스프링 빈 등록

  • 스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록합니다.

빈 이름

  • 빈 이름은 메서드 이름을 사용합니다. (default)
  • 빈 이름을 직접 부여할 수도 있습니다.
    • @Bean(name="원하는 이름")
  • 주의) 빈 이름은 항상 다른 이름을 부여해야 합니다. 같은 이름을 부여할 경우 빈이 무시되거나, 기존 빈을 덮어버리는 오류가 생길 수 있습니다.

 

3. 스프링 빈의 의존관계 설정 - 준비
4. 스프링 빈 의존관계 설정 - 완료

  • 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입(DI)합니다.
  • 단순히 자바 코드를 호출하는 것 같지만 차이가 있습니다. 이 차이는 후에 싱글톤 컨테이너에서 이해할 수 있다고 합니다.

 

참고

스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나뉘어져 있습니다. 그런데 이렇게 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리됩니다. 

 

정리

스프링 컨테이너를 생성하고, 설정(구성) 정보를 참고해서 스프링 빈도 등록하고, 의존 관계도 설정했습니다. 이제 스프링 컨테이너에서 데이터를 조회해보고자 합니다.

 

 

 

컨테이너에 등록된 모든 빈 조회

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for(String beanDefinitionName : beanDefinitionNames){
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName + "object = " + bean);
        }
    }

    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for(String beanDefinitionName : beanDefinitionNames){
            BeanDefinition bean = ac.getBeanDefinition(beanDefinitionName);

            //Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
            //Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
            if(bean.getRole() == BeanDefinition.ROLE_APPLICATION) {
                System.out.println("name = " + beanDefinitionName + "object = " + bean);
            }
        }
    }
  • 모든 빈 출력하기
    • ac.getBeanDefinitionNames() : 스프리에 등록된 모든 빈 이름을 조회합니다.
    • ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회합니다.
  • 애플리케이션 빈 출력하기
    • getRole()로 등록한 빈, 스프링 내부 빈인 것을 확인할 수 있습니다.
    • BeanDefinition.ROLE_APPLICATION : 일반적으로 상요자가 정의한 빈 입니다.
    • BeanDefinition.ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈 입니다.

 

 

스프링 빈 조회 - 기본

스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회방법

  • ac.getBean(빈이름, 타입)
  • ac.getBean(타입)
  • 조회 대상 스프링 빈이 없으면 예외 발생
    •      NoSuchBeanDefinitionException: No bean named 'xxxxx' available
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("이름 없이 타입으로만 조회")
    void findBeanByType() {
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("구체 타입으로 조회")
    void findBeanByName2() {
        MemberService memberService = ac.getBean("memberService",MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByEmptyName() {
        //MemberService memberService = ac.getBean("xxxxx",MemberService.class);
        //assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
        assertThrows(NoSuchBeanDefinitionException.class,
                () -> ac.getBean("xxxxx",MemberService.class));
    }

참고로 구체 타입으로 조회하면 변경시 유연성이 떨어집니다.

 

 



스프링 빈 조회 - 동일한 타입이 둘 이상

  • 타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생합니다. 이때는 빈 이름을 지정하면 됩니다.
  • ac.getBeansOfType()을 사용하면 해당 타입의 모든 빈을 조회할 수 있습니다.
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("타입으로 조시 같은 타입이 둘 이상 있으면, 중복 오류가 발생합니다.")
    void findBeanByTypeDulicate() {
        // 이렇게 하면 에러가 터집니다. 어떤 MemberRepository를 빈에서 가져와야 하는지 모르기 때문입니다.
        //MemberRepository memberRepository = ac.getBean(MemberRepository.class);

        assertThrows(NoUniqueBeanDefinitionException.class,
                () ->ac.getBean(MemberRepository.class));
    }

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다") void findBeanByName() {
        MemberRepository memberRepository = ac.getBean("memberRepository1",
                MemberRepository.class);
        assertThat(memberRepository).isInstanceOf(MemberRepository.class);
    }

    @Test
    @DisplayName("특정 타입을 모두 조회하기")
    void findAllBeanByType() {
        Map<String, MemberRepository> beansOfType =
                ac.getBeansOfType(MemberRepository.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " +
                    beansOfType.get(key));
        }
        System.out.println("beansOfType = " + beansOfType);
        assertThat(beansOfType.size()).isEqualTo(2);
    }


    @Configuration
    static class SameBeanConfig {

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

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

 

 

스프링 빈 조회 - 상속 관계

  • 부모 타입으로 조회하면, 자식 타입도 함께 조회합니다.
  • 그래서 모든 자바 객체의 최고 부모인 Object 타입으로 조회하면, 모든 스프링 빈을 조회합니다.

이미지 대로 x번 조회하면 조회되는 번호입니다.

 

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상있으면, 중복 오류가 발생합니다.")
    void findBeanByParentTypeDuplicate() {
        //DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
        Assertions.assertThrows(NoUniqueBeanDefinitionException.class, () ->
                ac.getBean(DiscountPolicy.class));
    }

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상있으면, 빈 이름을 지정하면 됩니다.")
    void findBeanByParentTypeBeanName() {
        DiscountPolicy rateDiscountPolicy = ac.getBean("RateDiscountPolicy", DiscountPolicy.class);
        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeanBySubType() {
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기")
    void findAllBeanByParentType() {
        Map<String, DiscountPolicy> beansOfType =
                ac.getBeansOfType(DiscountPolicy.class);
        assertThat(beansOfType.size()).isEqualTo(2);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value=" +
                    beansOfType.get(key));
        }
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기 - Object")
    void findAllBeanByObjectType() {
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value=" +
                    beansOfType.get(key));
        }
    }


    @Configuration
    static class TestConfig {

        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }

        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FIxDiscountPolicy();
        }
    }

 

 

 

BeanFactory와 ApplicationContext

  • BeanFactory
    • 스프링 컨테이너의 최상위 인터페이스입닌다.
    • getBean()을 제공합니다.
  • ApplicationContext
    • BeanFactory 기능을 모두 상속받아서 제공합니다. 
    • ApplicationContext는 BeanFactory가 제공하는 기능을 포함한 수많은 부가기능을 제공합니다.
    • 메시지소스를 활용한 국제화 기능
      • 예를 들어서 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력합니다.
    • 환경변수
      • 로컬, 개발, 운영등을 구분해서 처리합니다.
    • 애플리케이션 이벤트
      • 이벤트를 발행하고 구독하는 모델을 편리하게 지원합니다.
    • 편리한 리소스 조회
      • 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회합니다.

 

 

다양한 설정 형식 지원 - 자바 코드, XML

스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있습니다.

어노테이션 기반 자바 코드 설정 사용

  • 현재까지 했던 것 입니다.

 

XML 설정 사용

  • 최근에는 스프링 부트를 많이 사용하면서 XML 기반의 설정은 잘 사용하지 않는다고 합니다. 아직 많은 레거시 프로젝트들이 XML로 되어있고, 또 XML을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점도 있으므로 한번쯤 배워두는 것도 좋다고 합니다.
     
  • GenericXmlApplicationContext를 사용하면서 xml 설정 파일을 넘기면 된다고합니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="memberService" class="hello.core.member.MemberServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
    </bean>
    <bean id="memberRepository"
          class="hello.core.member.MemoryMemberRepository" />
    <bean id="orderService" class="hello.core.order.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
        <constructor-arg name="discountPolicy" ref="discountPolicy" />
    </bean>
    <bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy" />

</beans>

위는 xml 코드입니다. application.properties가 있는 resources폴더 안에 xml 파일로 생성한 뒤 복붙하시면 좀 더 편하게 테스트 해보실 수 있을 것 같습니다. 보시면 자바 코드로된 AppConfig.java 설정 정보와 비슷하다는 것을 알 수 있습니다.

 

테스트 하는 코드는 기존의 코드에서 구현체와 파라미터를 수정해주시면 됩니다.

        ApplicationContext ac = new GenericXmlApplicationContext("AppConfig.xml");

 

필요하면 스프링 공식 레퍼런스 문서를 확인해보시면 좋을 것 같습니다.

https://spring.io/projects/spring-framework

 

Spring Framework

 

spring.io

 

 

 

스프링 빈 설정 메타 정보 - BeanDefinition

  • 스프링은 어떻게 이런 다양한 설정 형식을 지원하는 걸까요?? 그 중심에는 BeanDefinition 이라는 추상화가 있습니다.
  • 쉽게 이야기해서 역할과 구현을 개념적으로 나눈 것 입니다.
    • XML 또는 자바 코드를 읽어서 BeanDefinition을 만들면 됩니다.
    • 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 됩니다. 오직 BeanDefinition을 알고있을 뿐 입니다.
  • BeanDefinition을 빈 설정 메타정보라 합니다.
    • @Bean, <bean> 당 각각 하나씩 메타 정보가 생성됩니다.
  • 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성합니다.

코드 레벨로 조금 더 깊게 보면

  • AnnotationConfigApplicationContext는 AnnotatedBeanDefinitionReader를 사용해서 AppConfig.class를 읽고 BeanDefinition을 생성합니다.
  • GenericXmlApplicationContext는 XmlBeanDefinitionReader를 사용해서 appConfig.xml 설정 정보를 읽고 BeanDefinition을 생성합니다.
  • 새로운 형식의 설정 정보가 추가되면, XxxBeanDefinitionReader를 만들어서 BeanDefinition을 생성하면 됩니다.

 

BeanDefinition 살펴보기

 

BeanDefinition 정보

  • 실제로 Bean에 등록된 객체를 출력할 때 나오는 출력 값 입니다.
beanDefinitionName = memberService  // 생성할 빈의 클래스 명
beanDefinition = Root 
bean: class [null]; 	
scope=; 			// 싱글톤(기본값)
abstract=false; 
lazyInit=null; 		// 스프링 컨테이너를 생서할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부
autowireMode=3; 
dependencyCheck=0; 
autowireCandidate=true; 
primary=false; 
factoryBeanName=appConfig; 		// 팩토리 역할의 빈을 사용할 경우, 예) appConfig
factoryMethodName=memberService;	// 빈을 생성할 팩토리 메서드 지정, 예) memberService
initMethodName=null; 		// 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
destroyMethodName=(inferred); 		// 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명

defined in hello.core.AppConfig

 

BeanDefinition 정리

  • BeanDefinition에 대해서는 너무 깊이있게 이해하기보다는, 스프링이 다양한 형태의 설정 정보를 BeanDefinition으로 추상화해서 사용하는 것 정도만 이해하면 좋을 것 같다고 합니다.

 

 

 

 

출처

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

목표

  • 순수한 자바를 이용하여 역할(인터페이스)와 구현(구현체)를 나누어서 개발을 해보며 객체 지향 프로그래밍과 스프링에 대해 이해합니다.
  • 실제 요구사항에 맞춰서 유연하게 변경이 가능한가를 확인하여 자바의 다형성에 대해 이해합니다. (스프링에서의 개방-폐쇄, 의존관계 역전 원칙이 잘 지켜지는지 확인합니다.)

 

 

 

프로젝트 생성

아래의 웹사이트에서 스프링 부트 프로젝트를 생성할 수 있습니다.

 

해당 웹사이트에서 아래와 같이 프로젝트 언어, 부트 버전(SNAPSHOT은 정식 릴리즈되지 않은 버전입니다.), 메타데이타, 디펜던시(아무것도 선택하지 않으면 기본적인 코어만 가지고 프로젝트가 생성된다고 합니다.)를 선택해 주었습니다.

해당 페이지 하단에 GENERATE를 클릭하면 선택한 버전대로 스프링부트 프로젝트가 생성됩니다. 저는 IntelliJ, Java 11버전을 통해 프로그램을 실행시켰습니다. 버전이 다른 경우 잘 되지 않을수도 있다고 합니다.

 

 

 

비즈니스 요구사항과 설계

요구사항

  • 회원
    • 회원을 가입하고 조회할 수 있습니다.
    • 회원은 일반과 VIP 두 가지 등급이 있습니다.
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있습니다. (확정되지 않은 부분입니다.)
  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있습니다.
    • 회원 등급에 따라 할인 정책을 적용할 수 있습니다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용합니다. (나중에 변경될 수 있는 부분입니다.)
    • 할인 정책은 변경 가능성이 높습니다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶어합니다. 최악의 경우 할인을 적용하지 않을 수 도 있습니다. (할인은 확정되지 않은 부분입니다.)

요구사항을 보면 확정되지 않은 부분은 지금 결정하기 어려운 부분이라고 가정합니다. 하지만 결정될 때 까지 개발자는 기다릴 수 없다는 가정을 두고선 앞서 배운 객체 지향 설계 방법에 따라서 개발을 진행해보도록 하겠습니다. 현재는 스프링이 없는 순수한 자바로만 개발을 진행할 것 입니다.

 

 

 

회원 도메인 설계

앞서 말한 회원에 대한 요구 사항에 맞춰 설계합니다.

 

  • 회원 도메인 협력 관계

회원 저장소인 회원데이터에 접근하는 부분을 interface로 두는 이유는 자체 DB를 구축할지, 외부 시스템과 연동할지 정확하게 확정되지 않은 부분이기 때문입니다. 확정되지 않은 구현체는 메모리 회원 저장소라는 임의의 구현체를 통해 개발을 진행하도록 하였습니다.

 

  • 회원 클래스 다이어그램

위의 회원 도메인을 실제 구현할 다이어그램입니다. 회원서비스는 고려하는 여러개의 구현체가 없기때문에 MemberServiceImpl이라는 구현체로 구현하고, 회원데이터를 저장할 회원저장소인 MemberRepository 인터페이스의 구현체는 MemoryMemberRepository라는 임의의 구현체로 구현합니다. 추후에 확장하기위해 DbMemberRepository라는 자체 DB구현체와 그림에는 없지만 외부시스템 구현체도 포함합니다.

 

  • 회원 객체 다이어그램

 

 

 

테스트 프레임워크

애플리케이션 로직으로 테스트를 하는 것은 좋은 로직이 아니기 떄문에 Junit이라는 테스트 프레임워크를 사용하여 작성한 코드의 테스트를 진행해보겠습니다. 

 

먼저 모든 코드는 위의 회원 클래스 다이어그램에 따라서 작성이된 후 입니다.

 

main패키지가 아닌 test패키지에 hello.core패키지 안에 member라는 패키지를 만들고 MemberServiceTest라는 멤버서비스를 테스트할 객체를 생성합니다. 그 후 given,when,then에 따라 테스트 코드를 작성한 뒤 테스트를 진행해줍니다. 실행했을 때 정상적으로 프로그램이 종료되면 테스트는 성공적으로 끝난 것 입니다.

package hello.core.member;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

public class memberServiceTest {
    MemberService memberService = new MemberServiceImpl();

    @Test
    void join() {
        //given      ~~이런게 주어지고
        Member member = new Member(1L, "memberA", Grade.VIP);

        //when      ~~이렇게 했을 때
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        //then        이렇게 된다.
        Assertions.assertThat(member).isEqualTo(findMember);
    }

}

 

 

 

회원 모메인 설계의 문제점

위의 설계상 문제점은 무엇일까요?? 현재까지 작성한 코드는 순수한 자바로 작성된 코드입니다. 전에 이해한 다형성으로 해결하지 못하는 개방-폐쇄, 의존관계 역전 원칙을 여전히 잘 지키지 못하는 모습을 볼 수 있습니다. 

 

 

 

주문과 할인 도메인 설계

  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있습니다.
    • 회원 등급에 따라 할인 정책을 적용할 수 있습니다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용합니다. (나중에 변경될 수 있는 부분입니다.)
    • 할인 정책은 변경 가능성이 높습니다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶어합니다. 최악의 경우 할인을 적용하지 않을 수 도 있습니다. (할인은 확정되지 않은 부분입니다.)

위의 정책에 따라서 설계합니다. 

 

 

  • 주문 도메인 협력, 역할, 책임

역할만 그린 그림입니다.

 

 

  • 주문 도메인 전체

역할과 구현을 분리했기때문에 자유롭게 구현 객체를 조립할 수 있게 설계했습니다. 덕분에 회원 저장소는 물론이고, 할인 정책도 유연하게 변경할 수 있습니다.

 

 

  • 주문 도메인 클래스 다이어그램

 

  • 주문 도메인 객체 다이어그램1

회원을 메모리에서 조회하고, 정액 할인 정책(고정 금액)을 지원해도 주문 서비스를 변경하지 않아도 됩니다. 역할들의 협력관계를 그대로 재사용할 수 있습니다. 이는 MemoryMemberRepository가 DbMemberRepository로 변경되도 주문 서비스 구현체는 변경하지 않아도 된다는 것을 의미합니다.

 

  • 주문 도메이 객체 다이어그램2

회원을 메모리가 아닌 실제 DB에서 조회하고, 정률 할인 정책(주문 금액에 따라 % 할인)을 지원해도 주문 서비스를 변경하지 않아도 됩니다. 즉, 협력 관계를 그대로 재사용할 수 있습니다.

 

 

 

객체 지향 원리 적용

새로운 할인 정책 개발

악덕 기획자가 서비스 오픈 직전에 할인 정책을 지금처럼 고정 금액 할인이 아니라 좀 더 합리적인 주문 금액당 할인하는 정률 % 할인으로 변경하고 싶다고 합니다. 이 때 객체 지향 설계 원칙을 준수하지 않았다면 많은 코드를 수정하겠지만, 유연한 설계가 가능한 객체지향 설계 원칙을 준수했다면 쉽게 요구사항을 변경할 수 있습니다.

 

  • RateDiscountPolicy 추가

기존 고정 할인 금액이던 FIxDiscountPolicy 구현체를 RateDidscountPolicy로 변경합니다.

 

 

새로운 할인 정책 적용과 문제점

public class OrderServiceImpl implements OrderService {
  //    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
      private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
  }

할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 클래스의 코드를 고쳐야 합니다. 여기서 문제점은 개방-폐쇄, 의존관계 역전 원칙을 준수하려고 했지만, 실제로 지켜지지 못한 모습을 볼 수 있습니다. 

  • 의존관계 역전 원칙 : 주문 서비스 클라이언트(OrderServiceImpl)는 DiscountPolicy 인터페이스에 의존하면서 의존관계 역전 원칙을 지킨 것 같지만, 인터페이스 뿐만 아닌 구현체 클래스에도 의존하는 모습을 볼 수 있습니다.
  • 개방-폐쇄 원칙 : 변경하지 않고 확장하면 된다고 했지만, 클라이언트 코드를 변경해야하는 모습을 볼 수 있습니다.

 

왜 클라이언트 코드를 변경해야 될까??

  • 기대했던 의존관계

지금까지 단순히 DiscountPolicy 인터페이스만 의존한다고 생각하며 코딩을 했습니다.

 

  • 실제 의존관계

하지만, 인터페이스 뿐만이 아닌 구현체에도 의존하고 있는 모습을 볼 수 있습니다. (의존관계 역전 위반)

 

  • 구현체 변경

또한, 구현체를 변경하는 순간 OrderServiceImpl의 소스 코드를 함께 변경해야 합니다. (개방-폐쇄 원칙 위반)

 

 

이 문제를 해결하기 위해서는 클라이언트가 추상(인터페이스)에만 의존하도록 변경하면됩니다. 

 public class OrderServiceImpl implements OrderService {
      //private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
      private DiscountPolicy discountPolicy;
}

실제로 인터페이스에만 의존하도록 코드를 변경해보면 위와 같이 변경할 수 있습니다. 하지만, 실제 실행해보면 Null pointer exception이 발생합니다. 그러면 두 원칙을 지킬 수 없는 것인가??에 대한 의문이 듭니다.

 

 

이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl 클래스에 DiscountPolicy의 구현체를 대신 생성하고 주입해주면 가능합니다.

 

 

관심사의 분리

현재까지 구현한 애플리케이션을 하나의 공연에 빗대어서 생각해봅시다. 로미오와 줄리엣 공연을 하면 로미오, 줄리엣 역할을 누가할지는 배우들이 정하는게 아닙니다. 애플리케이션도 마찬가지입니다. 하지만, 현재까지 구현한 코드는 로미오 역할(인터페이스)를 하는 무명 배우(구현체, 배우)가 줄리엣 역할(인터페이스)을 하는 무명 배우(구현체, 배우)를 직접 초빙하는 것과 같습니다. 이를 해결하기 위해서는 관심사를 분리할 필요가 있습니다.

 

 

관심사를 분리하자

  • 배우는 본인의 역할인 배역을 수행하는 것에만 집중해야 합니다.
  • 남자 주인공은 어떤 여자 주인공이 선택되더라도 똑같이 공연을 할 수 있어야합니다.
  • 공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 나올 시점입니다.
  • 공연 기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 분리합시다.

 

AppConfig 등장

애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스 입니다.

package hello.core;

import hello.core.discount.FIxDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
 
public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService() {
        return new OrderServiceImpl(
                new MemoryMemberRepository(),
                new FIxDiscountPolicy();
    }

}

AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성하며, 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입(연결)해 줍니다.

 

아래 코드를 보면 실제 클라이언트에서는 구현체에 대한 코드가 포함되지 않고 정상적으로 돌아가는 것을 볼 수 있습니다.

public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

 

  • 클래스 다이어그램

&amp;amp;nbsp;

  • 객체의 생성과 연결은 AppConfig가 담당합니다.
  • 의존관게 역전 원칙 완성 : MemberServiceImpl은 MemberRepository인 추상에만 의존하면 됩니다.
  • 관심사의 분리 : 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었습니다.

 

  • 회원 객체 인스턴스 다이어그램

  • appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달합니다.
  • 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고해서 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라 합니다.

 

AppConfig 리팩터링

현재 AppConfig를 보면 중복이 있고, 역할에 따른 구현이 잘 보이지 않습니다.

  • 기대하는 그림

 

  • 리펙터링 전
package hello.core;

import hello.core.discount.FIxDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService() {
        return new OrderServiceImpl(
                new MemoryMemberRepository(),
                new FIxDiscountPolicy());
    }

}

new MemoryMemberRepository()의 중복이 있습니다. 또한, 역할에 따른 구현이 잘 보이지 않습니다.

 

 

  • 리펙터링 후
package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FIxDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(getMemberRepository());
    }

    private MemberRepository getMemberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService() {
        return new OrderServiceImpl(
                getMemberRepository(),
                getDigcountPolicy());
    }

    public DiscountPolicy getDigcountPolicy() {
        return new FIxDiscountPolicy();
    }
}

new MemoryMemberRepository()의 중복을 제거하였고, MemoryMemberRepository를 다른 구현체로 변경할 때 한 부분만 변경하면 되도록 하였습니다. 또한, 역할과 구현 클래스가 한 눈에 들어옵니다. 이를 통하여 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있습니다.

 

 

 

새로운 구조와 할인 정책 적용

기존 고정액 할인 정책을 정률 % 할인 정책으로 변경하는 일을 하려고합니다.

 

  • 구성의 분리

 

  • 할인 정책의 변경

FixDiscountPolicy -> RateDiscountPolicy로 변경해도 구성 영역만 영향을 받기때문에 사용 영역의 다른 코드는 바꾸지 않아도 됩니다.

 

 

  • 실제 바꾼 코드
package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FIxDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(getMemberRepository());
    }

    private MemberRepository getMemberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService() {
        return new OrderServiceImpl(
                getMemberRepository(),
                getDigcountPolicy());
    }

    public DiscountPolicy getDigcountPolicy() {
        //return new FIxDiscountPolicy();
        return new RateDiscountPolicy();
    }
}

실제로 주석만 한줄 만들고, 새로운 한줄만 추가하면 프로젝트는 정상적으로 돌아갑니다.

 

 

 

정리

좋은 객체 지향 설계의 5가지 원칙의 적용

여기서 3가지 SRP, DIP, OCP를 적용했습니다.

 

SRP - 단일 책임 원칙

한 클래스는 하나의 책임만 가져야 한다.

  • 클라이언트 객체는 직접 구현 객체를 생성, 연결, 실행하는 다양한 책임을 가지고 있었습니다.
  • SRP 단일 책임 원칙을 따르면서 관심사를 분리했습니다.
  • 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당합니다.
  • 이에따라 클라이언트 객체는 실행하는 책임만 담당합니다.

 

DIP - 의존관게 역전 원칙

프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다."  의존성 주입(DI)은 이 원칙을 따르는 방법 중 하나 입니다.

  • 새로운 할인 정책을 개발하고, 적용하려고 하니 클라이언트 코드도 함께 변경해야 했습니다. 이유는 클라이언트는 추상화 인터페이스에만 의존하는 것 같았지만, 구체화 구현 클래스도 함꼐 의존했기 떄문이였습니다.
  • 클라이언트 코드가 추상화 인터페이스에만 의존하도록 할 필요가 있었습니다.
  • 하지만 클라이언트 코드는 인터페이스만으로는 아무것도 실행할 수 없었습니다.(구현체를 넣지 못했기 떄문)
  • 이를 해결하기 위해 AppConfig를 만들어주었고, 구현 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입하는 방식으로 해결했습니다. 이에따라 DIP 원칙을 따르게 할 수 있었습니다.

 

OCP - 개방폐쇄 원칙

소프트웨어 요스는 확장에는 열려있으나 변경에는 닫혀 있어야 한다.

  • 다형성 사용하고 클라이언트가 DIP를 지킵니다.
  • 애플리케이션을 사용 영역과 구성영역으로 나누어주었습니다.
  • AppConfig가 의존관계를 변경해서 클라이언트 코드에 주입하기 때문에 클라이언트 코드는 변경하지 않아도 됩니다.

 

 

 

 

 

출처

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

목표

  • 스프링을 왜 만드는가?
  • 이유와 핵심원리
  • 스프링 기본 기능 학습
  • 스프링 본질 깊은 이해
  • 객체 지향 설계에 대한 고민을 할 수 있게 해줌

 

 

서론

2000년대 초반에는 자바 정파 기술에는 EJB(Enterprise Java Beans)가 있었습니다. EJB는 이론, 분산, 트랜잭션 면에서 다 좋았지만, 개발자들이 배우기에 정말 어렵고 복잡할 뿐만아니라 속도도 느렸습니다. 결론적으로 개발자가 사용하기에 단순하고 편하지 않았습니다. 따라서 선배개발자분들이 사용하기 어려워했다고 합니다. EJB에 불편함을 느끼던 다른 개발자들 중 2명이 각각 새로운 오픈소스를 만들었습니다. 2명의 개발자 중 한 명은 SI 개발자였습니다. 이름은 Rod Johnson이였고 Rod Johnson은 EJB를 비판하며 더 단순하고 좋은 방법으로 개발할 수 있다며 책을 통해 공개한 오픈 소스 3만줄이 미래에 Spring이 됩니다. 또 2명의 개발자 중 다른 한 명인 Gavin King은 EJB 엔티티빈 기술을 사용하면서 불편함을 느꼇고, 해당 기술을 더 편리하게 사용할 수있는 Hibernate를 개발합니다. 후에 Hibernate는 JPA 인터페이스의 구현체로 사용됩니다. 현재 JPA시장의 80%는 Hibernate를 사용한다고 합니다. Spring의 시작은 Rod Johnson의 오픈 소스 책을 읽은 Juerhen hoeller(유겐 휠러)Yann Caroff(얀 카로프)가 오픈 소스 프로젝트를 제안하며 시작했고, EJB라는 개발 지옥의 겨울을 넘어 개발자들에게 봄이 왔다는 뜻으로 이름을 Spring이라고 짓게 되었다고 합니다.

 

 

스프링 생태계

  • 필수
    • 스프링 프레임워크
      • 핵심 기술 : 스프링 DI 컨테이너, AOP, 이벤트, 기타
      • 웹 기술 : 스프링 MVC, 스프링 WebFlux
      • 데이터 접근 기술 : 트랜잭션, JDBC, ORM 지원, XML 지원
      • 기술 통합 : 캐시, 이메일, 원격접근, 스케줄링
      • 테스트 : 스프링 기반 테스트 지원
      • 언어 : 코틀린, 그루비
      • 최근에는 스프링 부트를 통해서 스프링 프레임워크의 기술들을 편리하게 사용
    • 스프링 부트
      • 스프링을 편리하게 사용할 수 있도록 지원, 최근에는 기본으로 사용
      • 단독으로 실핼할 수 있는 스프링 애플리케이션을 쉽게 생성
      • Tomcat 같은 웹 서버를 내장해서 별도의 웹 서버를 설치하지 않아도 됨 - 옛날에는 빌드 한 후 톰캣서버 특정 위치에다가 빌드한 스프링 프로젝트를 넣고 띄우고 등등 복잡했다고 합니다.
      • 손쉬운 빌드 구성을 위한 starter 종속성 제공 - 라이브러리 하나를 쓸 때 여러개의 라이브러리를 땡겨왔어야 했는데 이를 쉽게 스타터를 통해 쉽게 해줍니다.
      • 스프링과 3rd parth(외부) 라이브러리 자동 구성
      • 메트릭, 상태 확인, 외부 구성 같은 프로덕션 준비 기능 제공
      • 관례에 의한 간결한 설정 - 스프링 프레임워크가 설정이 힘들었다고 합니다. 이를 보완
  • 선택
    • 스프링 데이터
      • RDBMS, NoSQL, mongoDB, Redis 등 기본적인 CRUD(등록, 수정, 삭제, 조회)는 비슷합니다. 이러한 DB를 편리하게 사용할 수 있도록 도와줍니다. 기본적으로 스프링 데이터 JPA를 가장 많이 사용합니다.
    • 스프링 세션
      • 세션 기능을 편리하게 사용할 수 있도록 도와줍니다.
    • 스프링 시큐리티
      • 보완을 도와줍니다.
    • 스프링 Rest Docs
      • API문서와 Test를 묶어서 API 문서화를 편리하게 해줍니다.
    • 스프링 배치
      • 실제 천 만명의 데이터를 한번에 업데이트를 해야할 때 실시간으로 하기 어려움이 있다고합니다. 이럴 때 천만 건 중 몇 건씩 돌리고 저장을 반복하는 것을 배치처리라고 합니다. 이러한 처리를 담당하는 곳 입니다.
    • 스프링 클라우드
      • 최근 클라우드 기술에 특화된 기술입니다.

 

 

 

스프링이란 기술은 왜 만들었고 왜 필요한가?

Rod Johnson이 약 3만 줄의 오픈 소스를 만들기 전 EJB를 사용하던 개발자들은 EJB를 상속받고하면서 EJB에 의존적으로 개발을 해야했습니다. 이를통해 객체지향이 가진 좋은 장점들을 다 잃어버리게 됩니다.(이 부분에 대해서는 필자도 어떠한 장점들을 잃어버리게 되는지 정리해야될 것 같습니다.) 따라서 당시 순수한 자바로 돌아가고자 POJO라는 단어도 나오기도 했습니다. 결론적으로 객체지향의 장점들을 잘 살려내기 위해 스프링이란 기술이 등장하게 되었습니다.

스프링은 JAVA 언어 기반의 프레임워크입니다. JAVA 언어의 가장 큰 특징은 객체 지향 언어 입니다. 스프링은 객체 지향 언어가 가진 강력한 특징을 살려내는 프레임워크입니다. 정리하자면 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크입니다. 

 

 

 

좋은 객체 지향 프로그래밍이란?

객체 지향 프로그래밍에는 추상화, 상속화, 다형성, 캡슐화로 총 4가지의 특징이 있습니다. 볼 때마다 설명하기 어려운 것 같아 이번에 확실하게 정리하고자 합니다. 객체 지향 프로그래밍의 정의는 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위인 객체 들의 모임으로 파악하고자 하는 것 입니다. 각각의 객체는 메시지를 주고받고 데이터를 처리할 수 있습니다. 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용됩니다.

 

 

 

유연하고 변경이 용이하다?

유연하고 변경이 용이하다가 잘 와닿지 않을 거라고 생각합니다. 유연하고 변경이 용이하다는 것은 레고 블럭을 조립하듯이 또는 키보드, 마우스를 갈아 끼우듯컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 것을 의미합니다. 이것을 가능하게 해주는 것이 객체 지향 프로그래밍의 특징 중 하나인 다형성입니다.

 

 

 

다형성

실세계와 객체 지향을 1:1로 매칭할 순 없지만 이해하기는 좋기때문에 실세계를 비유해서 이해시켜보도록 하겠습니다. 역할해당 역할의 구현으로 세상을 나눠서 생각해봅시다. 해당 역할을 운전자와 자동차로 비유해서 다형성에 대해서 이해해보도록 하겠습니다.

운전자라는 역할이 있고 자동차라는 역할이 있습니다. 여기서 자동차 역할을 할 수 있는 구현체는 K3, 아반떼, 테슬라 모델3가 있습니다. 운전자는 K3를 타다가 아반떼로 차가 바뀌어도 운전을 할 수 있습니다. 그 이유는 자동차 역할의 구현만 바뀌었고 바뀐 자동차의 역할은 운전자에게 영향을 끼치지 않기 때문입니다. 이 점이 유연하고 변경이 용이하다고 하는 것 입니다. 쉽게 말해 운전자, 자동차 역할이 있을 때, 자동차가 어떤 것으로 바뀌던 간에 운전자는 운전을 할 수 있다는 것 입니다. 이 것을 인터페이스라는 개념으로 풀어서 설명하면 운전자가 있고 자동차 역할인터페이스가 있을 때 자동차 역할구현체를 어떤 자동차로 바꾸든 운전자는 자동차 역할이라는 인터페이스를 가지고 운전하기 때문에 운전이 가능하다는 것 입니다. 여기서 가장 중요한 것은 구현체인 새로운 자동차가 나와도 운전자는 바꾸지 않아도 된다는 것 입니다. 

 

역할구현으로 구분하면 세상이 단순해지고, 유연해지며 변경편리해집니다.

장점으로는 아래 4가지가 있습니다.

  • 클라이언트는 대상의 역할(즉, 인터페이스)만 알면 됩니다.
  • 클라이언트는 구현 대상의 내부 구조를 몰라도 됩니다.
  • 클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않습니다.
  • 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않습니다.

여기서 핵심은 구현체보단 역할인 인터페이스에 있습니다.

 

 

이제 객체 지향 프로그래밍에서의 실제 다형성의 특성으로 작성된 프로그램의 예시를 보겠습니다. 

위의 사진을 보며 오버라이딩을 떠올려봅시다. 오버라이딩에 대해 간략하게 설명하자면 만약 MemberService가 interface인 MemberRepository에 있는 save()를 호출할 때 실제 호출되는 save()는 MemoryMemberRepository와 JdbcMemberRepository중 들어와 있는 객체에 있는 save()가 호출되는 것 입니다. 인터페이스로 구현한 객체를 실행 시점에 유연하게 변경할 수 있는 것이 객체 지향 프로그래밍의 장점입니다. 

 

이미지로 설명한 예시
위의 이미지가 코드로 구현된 경우

다형성을 정리하자면 다형성의 본질은 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있다는 점입니다. 이것을 클라이언트 시점에서 보면 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다고도 볼 수 있습니다.

 

 

좋은 객체 지향 설계의 5가지 원칙(SOLID)

클린코드로 유명한 로버트 마틴이 좋은 객체 지향 설계의 5가지 원칙을 정리했습니다.

  • 단일 책임 원칙
    • 한 클래스는 하나의 책임만 가져야 합니다. 중요한 기준은 변경입니다. 즉, 하나를 변경할 때 최대한 하나만 변경하면 되도록 로직을 구현하면 됩니다.
  • 개방-폐쇄 원칙
    • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀있어야 합니다. 이게 무슨 모순인가 싶습니다. 이때 다형성을 생각해봅시다. 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현하는 것을 보면 기존 코드를 변경하지 않고 새로운 클래스를 통해 확장합니다. 하지만 위의 다형성으로 예시를 들면 구현 객체를 변경하는 경우 클라이언트 코드를 변경하기 때문에 개방-폐쇄 원칙을 지킬 수 없습니다. 이 문제를 해결하기 위해서는 별도의 조립을 해주는 설정자가 필요합니다. spring에서는 개발-폐쇄 원칙을 지키기위해 spring 컨테이너를 통해 해결해 준다고합니다. (현재는 이해가 잘 되지 않습니다.)
    • ex) 위의 코드로 예시를 들면
      MemberRepository member Repository = new MemoryMemberRepository();
      에서 구현체를 변경하면
      MemberRepository member Repository = new JdbcMemberRepository();
      MemoryMemberRepository 를 JdbcMemberRepository로 변경을 해야 합니다. 이렇게 때문에 개방-폐쇄 원칙을 어긴다고 볼 수 있습니다.
  • 리스코프 치환 원칙
    • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 합니다. 이를 간단하게 얘기하면 어떤 인터페이스가 있고, 구현체가 있다고 가정합니다. 자동차를 예로들면 자동차 인터페이스에 엑셀(앞으로 빠르게 가는 기능)이 있습니다. 엑셀의 본질적인 정확한 기능은 앞으로 빠르게 가는 기능입니다. 하지만 구현체인 K3가 엑셀의 기능을 멈추는 기능으로 만들어 버릴경우 리스코프 치환 원칙을 위배했다는 것이라고 보면됩니다. 이처럼 리스코프 치환 원칙은 인터페이스에서 정의한 본질적인 기능을 구현체에서 깨뜨리지 않는 것을 의미합니다.
  • 인터페이스 분리 원칙
    • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다를 말합니다. 
    • 예를 들어, 자동차 인터페이스를 운전 인터페이스, 정비 인터페이스로 분리합니다.
    • 이에 따라, 운전자 클라이언트, 정비사 클라이언트로 분리할 수 있게 됩니다.
    • 이렇게 분리하는 경우 정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않기 때문에 인터페이스가 보다 명확해지고, 대체 가능성도 높아지게 됩니다.
  • 의존관계 역전 원칙
    • 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나 입니다. 쉽게 이야기하면 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻입니다. 앞에서 이야기한 역할에 의존하게 해야 한다는 것과 같습니다. 위에 말한 운전자와 자동차로 다시 예시를 들면 운전자는 자동차의 역할만 알면되지 구현체에 대해서는 몰라도 되게 해야한다는 것 입니다. 즉, 운전자는 자동차라는 역할에 의존하게 됩니다. 이를 통해 얻는 장점은 유연하게 구현체를 변경할 수 있습니다.

하지만, 다형성 만으로는 개방-폐쇄, 의존관계 역전 원칙을 지킬 수 없습니다. 

그 이유는 다형성 만으로는 위의 사진처럼 인터페이스인 MemberRepository 뿐만아니라 구현체인 MemoryMemberRepository도 클라이언트에서 알아야하기 때문입니다. 

 

 

 

객체 지향 설계와 스프링 

스프링은 다음 기술로 다형성 + 개방-폐쇄, 의존관계 역전 원칙을 가능하게 지원합니다.

  • DI(Dependency Injection) : 의존 관계, 의존성 주입
  • DI 컨테이너 제공

실제로 순수하게 자바로 개방-폐쇄, 의존관계 역전 원칙들을 지키면서 개발을 하다보면, 결국 스프링 프레임워크를 만들게 된다고합니다. (정확히는 DI 컨테이너) 이 때문에 스프링을 프레임워크로 만들었다고 합니다. 즉, SOLID 원칙 중 다형성으로 해결되지 않는 개방-폐쇄, 의존관계 역전 원칙 2가지를 지키기 위해 프레임워크를 만들었다고 보면 됩니다.

 

 

 

 

 

 

 

용어 정리

JPA - ORM기술 

ORM기술 - Java 객체를 DB에 편하게 저장하고 꺼내는 기술 (쿼리를 사용하지 않음.)

POJO(Plan Old Java Object) - 오래된 방식의 간단한 자바 오브젝트

 

 

책 추천 (김영한님의 추천입니다.)

객체지향 책 추천 : 객체지향의 사실과 오해 (주니어, 시니어 모두 도움 될 책)

스프링 책 추천 : 토비의 스프링 (필수 - 어느정도 공부한 뒤 책을 읽으면서 정리하면 분명 많은 도움이 된다고 합니다.)

JPA 책 추천 : 자바 ORM 표준 JPA 프로그래밍 (김영한님의 앞PPL...)

 

 

 

 

출처

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

네트워크에 공부한 뒤 공부한 내용을 복습할겸 겸사겸사 스프링 공부도 해보고 싶어서 Spring boot로 채팅 프로그램을 만들어 보려고 합니다. 일반적인 http통신을 하는 서버들과 달리 채팅 서버는 socket통신을 하는 서버가 필요합니다.

 

http통신

  • client의 요청이 있을 때만 서버가 응답하고 연결을 종료하는 단방햔 통신 이다.
  • 따라서 client가 server에 접속해 콘텐츠를 요청하고 결과를 받아 소비하는 구조의 서비스에서 많이 사용된다.

socket통신

  • server와 client가 특정 port를 통해 지속적으로 연결유지 하여 실시간으로 양방향 통신을 하는 방식
  • 주로 채팅 같은 실시간성을 요구하는 서비스에서 많이 사용된다.

Websocket

  • Websocket은 기존의 단방향 HTTP프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜
  • 일반 Socket통신과 달리 HTTP 80 port를 이용하므로 방화벽에 제약이 없다
  • 접속까지는 HTTP 프로토콜을 이용하고 그 이후의 통신은 자체적인 Websocket 프로토콜로 통신하게 된다.

프로젝트 생성

start.spring.io에서 선택한 버전과 dependencies 정보

  • 프로젝트 생성은 start.spring.io에서 만들었습니다.

 

 

기본 메인 메소드

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TestApplication {

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

}

프로젝트를 생성할 때 기본 메소드 그대로 사용하면 됩니다.

 

 

 

Websocket Handler 작성

import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.util.logging.Logger;

@Component
public class WebSocketChatHandler extends TextWebSocketHandler {

    private final static Logger LOG = Logger.getGlobal();

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String input = message.getPayload();
        LOG.info(input); // 채팅 log
        TextMessage textMessage = new TextMessage("Hello, 영진일지입니다. \n 웹소켓 테스트입니다.");
        session.sendMessage(textMessage);
    }
}

websocket 통신은 서버와 클라이언트가 1:N 으로 관계를 맺습니다. 따라서 한 서버에 여러 클라이언트가 접속 할 수 있으며, 서버에는 여러 클라이언트가 발송한 메시지를 받아 처리해줄 Handler의 작성이 필요합니다. 다음과 같이 TextWebSocketHandler 를 상속받아 Handler를 작성했습니다. Client로부터 받은 메시지를 콘솔에 출력하고 Client로 환영 메시지를 보내는 역할을 합니다.

 

 

 

Websocket Config 작성

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    private final WebSocketHandler webSocketHandler;

    public WebSocketConfig(WebSocketHandler webSocketHandler) {
        this.webSocketHandler = webSocketHandler;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler, "/ws/chat").setAllowedOrigins("*");
    }
}

방금전 만든 handler 를 이용하여 Websocket을 활성화 하기 위한 Config 파일을 작성했습니다.

@EnableWebSocket을 선언하여 Websocket을 활성화합니다.

Websocket에 접속하기 위한 endpoint는 /ws/chat으로 설정하고 도메인이 다른 서버에서도 접속 가능하도록 CORS 설정을 추가해줍니다. (여기서 endpoint 접근 url입니다. url:port번호 후 뒤에 붙는 문자열을 의미합니다.)

 

 

Websocket 테스트

 

테스트를 위한 클라이언트 대신 chrome 웹스토어에 있는 Simple Websocket Client를 설치하고 실행합니다.

Spring Boot 서버를 구동 후 설치한 확장프로그램 Simple WebSocket Client 를 실행하고 ws://localhost:8080/ws/chat 를 입력후 Open 을 누릅니다.

 

Websocket의 경우 별개의 프로토콜이므로 http 가 아닌 ws로 시작하는 주소체계를 갖습니다.

위는 Message Log창에 서버와 연결이 되어 서버로 부터 작성한 메세지가 오는 모습입니다.

 

또한 위의 사진은 스프링 서버에 전송온 Simple WebSocket Client에서 작성한 Request 메세지가 잘 전달된 모습입니다.

 

위와 같이 TCP/IP를 이용하여 클라이언트 대용인 Simple WebSocket Client와 서버인 Spring과의 3-way-handshake를 통한 연결 후 데이터를 전송하는 모습입니다.

'spring' 카테고리의 다른 글

[Spring] 빌드 툴(Ant, Maven, Gradle)  (0) 2021.10.28

빌드 툴이란?

  • 소스 코드의 빌드 과정을 자동으로 처리해주는 프로그램
  • 외부 소스코드(외부 라이브러리) 자동 추가 및 관리

 

빌드 툴이 왜 필요한가?

  • 대규모 프로젝트에선 빌드프로세스를 수동으로 호출이 실용적이지 않습니다.
  • 무엇을 빌드할지, 어떤 순서를 가지고 빌드할지, 어떤 의존성이 있는지 모두 추적하기 쉽지 않기때문입니다.
  • 빌드 툴을 사용하면 이를 일관되게 할 수 있습니다.

 

JAVA에서의 빌드 툴 종류

  • Ant
  • Maven
  • Gradle

 

Ant

  • 설정을 위해 xml을 사용합니다.
  • 간단하고 사용하기 쉽다고 합니다.
  • 단점
    • 복잡한 처리를 하려하면 빌드 스크립트가 장황해져 관리가 어렵습니다.
    • 외부 라이브러리를 관리하는 구조가 없다.

 

 

Maven

 

// xml의 예시 입니다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>com.example</groupId>
   <artifactId>demo-maven</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>

   <name>demo-maven</name>
   <description>Demo project for Spring Boot</description>

   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.5.4.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>

   <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <java.version>1.8</java.version>
   </properties>

   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter</artifactId>
      </dependency>

      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
      </dependency>
   </dependencies>

   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>


</project>
  • Ant가 가지고있는 단점을 보완하여 만들어진 빌드 툴입니다.
  • 설정을 위해 xml을 사용합니다.
  • 외부 라이브러리를 관리할 수 있습니다.
  • 장황한 빌드 스크립트 문제를 해결했습니다.
  • 단점
    • 특정 경우에 xml이 복잡해집니다.
    • xml 자체에 한계가 있습니다. (설정 내용이 길어지고 가독성 떨어짐)

 

 

Gradle

 

// gradle 예시

buildscript {
    ext {
        springBootVersion = '2.1.7.RELEASE'
    }
    repositories {
        mavenCentral()
        jcenter()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group 'com.jojoldu.book'
version '1.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}
  • Maven이 가지고있는 단점을 보완하여 만들어진 빌드 툴입니다.
  • 설정할 때 xml대신에 groovy 언어를 사용합니다. (xml이 가지는 한계를 갖지 않습니다.)
  • 에시만봐도 Maven보다 가독성 면에서 뛰어나다는 장점이 있습니다.
  • 외부 라이브러리를 관리할 수 있습니다.
  • 유연하게 빌드 스크립트를 작성할 수 있습니다.
  • 캐싱이 잘되어 성능이 뛰어납니다. (실제로 Gradle이 Maven보다 최대 100배 빠르다고 합니다.)

'spring' 카테고리의 다른 글

[Spring] 간단한 웹소켓 프로그램 만들기  (0) 2021.10.28

+ Recent posts