framework/spring

1. spring 핵심 요약

wooweee 2023. 5. 22. 13:54
728x90

0. 자세한 내용

 

 

1. appConfig

  • srp, dip, ocp 지키는 방법

  • 수동 빈 등록 방법

  • 종류
    • appConfig.java
    • appConfig.xml

  • @Configuration
    • @ComponentScan이 읽고 springContainer에 넣는다.
    • 가짜 객체를 통해서 싱글톤 및 싱글톤의 문제점을 해결

  • @Bean
    • @Bean으로 등록되는 type : interface
    • @Bean으로 반환하는 객체: interface의 구현체
    • DI하는 코드: 반환하는 객체 내부의 생성자 초기화 매개변수
    • @Bean의 이름은 겹치면 난리나니깐 무조건 다르게 지정하기

 

1.1. config.java 예시 코드

@Configuration // spring 설정정보
public class AppConfig {

    @Bean // spring container에 넣기
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
    
    // service
    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
}

 

1.2. config.xml 예시 코드

  • spring 레거시 프로젝트때는 xml 로 bean 등록을 권장
  • 직접 빈을 등록하기 때문에 @Configuration 같은 과정을 거칠 필요가 없다.
  •  필요하면  https://spring.io/projects/spring-framework 이 문서 찾아보기
<?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">
    // id : bean 이름
    // class : 해당 id의 실제 bean 객체
        <constructor-arg name="memberRepository" ref="memberRepository11"/>
        // constructor-arg : DI 해야 할 bean
        // ref : DI할 bean
        // name :생성자의 매개변수와 일치하는 이름
    </bean>

    <bean id="memberRepository11" 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.FixDiscountPolicy"/>
</beans>

 

 

2.Spring Container 및 bean 조회

  • 실제 code에선 잘 사용하지 않지만 TDD에 사용하므로 알아둘 필요가 있다.

2.1. SpringContainer

  • SpringContainer : applicationContext
  • SpringContainer 구현체 : annotationConfigApplicationContext(요즘), GenericXmlApplicationContext(xml)

 

2.2. bean 조회

  • 핵심 조회
    •  bean 1개 조회
      1. Object getBean(classType);
      2. Object getBean(String beanName);
    • 여러 bean 조회
      1. Map<String, classType> getBeansOfType(classType)

 

  • 빈 조회 method() 들
String[] getBeanDefinitionNames(); // spring 등록된 All bean 조회

Object getBean(String beanName); // 등록된 spring bean의 instance 반환

getBeanDefinition(String beanName); // 해당 bean instance의 meta 정보 반환
getRole(); meta 정보 반환된 객체에서 사용 가능, bean meta 정보 중 어디서 등록된 빈인지 정보 반환
// 상수
ROLE_APPLICATION // 직접 등록한 빈
ROLE_INFRASTRUCTURE // 스프링이 내부에서 사용하는 빈
// 사용 예
beanDefinition.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE // BeanDefinition : 해당 상수가 존재하는 class

classType getBean(classType); // 등록된 spring bean 중 해당 class의 bean instance 반환
classType getBean(String beanName, classType); // 해당 클래스에 여러개의 bean 1개의 bean만 반환
Map<String,classType> getBeansOfType(classType); // 해당 클래스에 여러개의 bean 존재시 모든 빈 반환

// classType도 구체타입, 추상타입 다 가능하지만 구체타입은 유연성이 떨어지므로 권장 안함
추상타입 : service.class
구체타입 : serviceImple.class

// classType으로 bean 호출 시 중복으로 bean이 나오는 경우
1. 해당 bean type이 동일할 때
2. classType의 자손 class type들이 등록 된 경우

 

  • 모든 빈 조회 tdd code
class ApplicationContextInfoTest1{
    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("app bean만 출력하기")
    void findApplicationBean(){
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName: beanDefinitionNames){

            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
            // getBeanDefinition: getRole()로 구분하기 위해 필요한 Bean에 관한 meta data 정보 반환

            // Role ROLE_APPLICATION: 직접 등록한 애플리케이선 빈
            // Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + "object = " + bean);
            }
        }
    }
}

 

 

3. singleton Container

  • java로 생성한 singleton Container 단점
    • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
    • 의존관계상 클라이언트가 구체 클래스에 의존한다.  
    •  DIP를 위반한다. ex) getInstance()로 직접 구현체 가져옴
    • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
    • 테스트하기 어렵다.
    • 내부 속성을 변경하거나 초기화 하기 어렵다.
    • private 생성자로 자식 클래스를 만들기 어렵다.
    • 결론적으로 유연성이 떨어진다.
  • springContainer는 위의 문제점을 다 해결한 singleton container를 만들었다.
    • @Configuration이 java singleton의 문제점을 해결하기 위해 사용하는 것, cglib라는 proxy객체를 사용하게 한다.

 

  • 주의점
    • 싱글톤이란 것은 주 목적은 공유객체이다.
    • 그래서 singleton은 최대한 stateless하게 설계해야한다.
      싱글톤인 class가 존재시, 어지간하면 method내의 지역변수를 통해서 무언가를 수행하고 정~~~말 아닐 경우 잘 생각하고 iv를 생성한다. iv가 결국 stateful한 것이기 때문

 

4. 자동 bean 등록방법

  • springboot, spring 레거시 동일
    • 차이점이라고 하면 springboot는 @ComponentScan이란 annotation으로 @Componet를 찾고 spring 레거시는 xml 파일 componentScan 을 등록한다.
  • @Component
  • @Autowired : getBean(classType)으로 찾기 때문에, 동일 type이 2개가 존재시 예외 발생

 

  • @ComponentScan 범위
    • default: @ComponentScan 붙은 설정 정보 class 패키지가 시작위치
    • 수동 : @ComponentScan(basePackages + "탐색패키지.하위프로젝트명")
    • springRegacy 경우: 수동의 방식으로만 등록할 수 있다.

  • @ComponentScan 필터
    • include, exclude가 있는데 있다는 정도로만 알기

 

  • 중복과 충돌
    • 스프링: 수동 빈 등록이 우선권을 가진다. 경고 문구만 날림(중복시 수동빈을 덮어쓰도록 의도하면서 코드를 작성하지 않으므로 주의 필요)

    • 부트: 에러 터뜨림 (최고방법)

 

5. 의존관계 자동 주입

  • 권장
    • 생성자 주입 99%
    • 수정자 주입 1%
    • 필드, 메서드 주입 (권장 안함)

5.1. 주입할 빈이 없는 경우

  •  옵션
    • 자동 의존관계 주입을 하려는 주입할 스프링 빈이 없을 경우 동작시키려고 하는 옵션 3가지
      1. @Autowired(required=false): 자동 주입 대상이 없을 시, 메서드 자체가 호출이 안된다.
      2. @Nullable : 자동 주입할 대상이 없으면 null이 입력
      3. Optional<> : 자동 주입할 대상이 없으면  Optional.empty 가 입력

 

  • lombok
    • 코들 간략화 최신 트랜드
    • getter, setter, 생성자, toString, requiredArgsConstructor, 등등

 

5.2. 조회 bean이 2개 이상

  • 예외 발생
    • @Autowired private DiscountPolicy discountPolicy
    • NoUniqueBeanDefinitionException
  • 해결 - 3가지 방법
    1. @Autowired 필드 명 - 거의 안쓰는 듯
    2. @Qualifier : sub (서브 설정 파일일 경우 사용)
    3. @Primary  : main (중요 설정 파일 경우 사용)

 

 

  • @Autowired 필드 명
    1. 타입 매칭 을 우선 수행
    2. 동일 타입 빈이 2개 이상일 때, 필드명, 파라미터 명과 동일 한 빈을 매칭
@Autowired
private DisountPolicy discountPolicy;

//@Autowired 필드 명
@Autowired
private DiscountPolciy rateDiscountPolicy;

//@Autowired 파라미터 명
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy){
    this.memberRepository = memberRepository;
    this.discoutnPolicy = ratediscountPolicy;
}

 

  • @Qualifier
    • 추가 구분자를 붙여주는 방법
    • 등록위치
      1. @Component - @Autowired 자동 주입
        1. 생성자 주입
        2. 수장자 주입
      2. AppConfig - @Bean에 주입 
        * 1번에서 끝나야지 2번까지 넘어가면 실수로인한 잡기 힘든 에러가 될 가능성이 높다.
    • annotaion 직접 만들어서 에러 줄이는 방법 존재

 

1. @Component - @Autowired 자동 주입 - 생성자 주입

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
 }
 
// 어노테이션 만든 경우
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy){
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

// 어노테이션 만들기

//@Qualifier에서 가져온 정보
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) 
@Retention(RetentionPolicy.RUNTIME) 
@Documented 
// 앞으로 사용할 임의의 애노테이션
@Qualifier("mainDiscountPolicy") 
public @interface MainDiscountPolicy { }

 

2. @Bean에 주입

 

@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy(){
    return new ...
}

 

 

  • @Primary
    • @Bean의 우선순위 정하는 방법
    • @Autowired시 여러빈이 매칭되면 @Primary를 가진 @Bean이 우선권을 가진다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

// 아래는 원래 주입코드와 동일함
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
	this.memberRepository = memberRepository;
	this.discountPolicy = discountPolicy;

 

6. 조회한 빈이 모두 필요할 때 - List, Map <중요>

 

  • 전략패턴 = 모든 빈 조회 방법 (tdd때 방법 말고 실제 spring main code에서 모든 빈 조회 방법) 
    1. Map<String, BeanType>
      • Map key = spring bean name
      • Map values = bean 객체
    2. List<DiscountPolicy>
      • bean 객체 나열
  • 사용 방법, 주석이 핵심 내용
public class AllBeanTest{

    @Test
    void findAllBean(){
    	ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
        
        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L, "userA", Grade.VIP);
        // fixDiscountPolciy라는 매개변수는 front단에서 받아올 것이다.
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
        
        asserThat(discountPrice).isInstanceOf(DiscountService.class);
        asserThat(discountPrice).isEqualTo(1000);
    }
    
    static class DiscountService{
        // bean 등록 DI 시, 전체 단위로 받는다.
    	private final Map<String, DisountPolicy> policyMap;
        private final List<DiscountPolicy> policies;
        
        @Autowired
        public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies){
            this.policyMap = policyMap;
            this.policies = policies;
        }
        
        public int discount(Member member, int price, String discountCode){
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member,price);
        }
    }
}

 

7. 빈 생명주기 callback

  • spring bean life cycle
스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 소멸전 콜백 -> 스프링 종료

# 초기화 콜백 : DI까지 끝내고 외부 호출 전 미리 수행하고 싶은 method 수행
# 소멸전 콜백 : spring 종료 직전 해당 작업이 먼저 안전히 종료하고 싶은 method() 수행

 

  • 의존관계 주입이 끝난 후 요청이 와야지 작업이 수행되지만 초기화 콜백 문법에 맞는 코드가 존재시, di까지 완료 후 초기화 콜백을 수행한다.
  • 소멸전 콜백 또한 마찬가지로 스프링이 끝나기 전에 소멸전 콜백에 맞는 문법이 적용이되었을 경우, 소멸전 콜백이 수행된다.

 

 

7.1. 초기화 콜백, 소멸전 콜백 문법

 

  1. 인터페이스 - 안씀
  2. @Bean - 3.번으로 해결 안될 경우 사용
  3. @PostConstruct, @Predestroy - 제일 많이 사용 (하지만 외부 library를 초기화, 소멸전 콜백 할 시 2번 사용 필요)

 

 

  • bean 
// NetworkClient 같은 경우는 직접 만든 class 이지만 외부 library를 가지고 올 경우도 있다.
// 이때 외부라이브러리 중 초기화 콜백할 method의 명만 알고 설정파일에 @Bean option에 넣어주면 된다.
public class NetworkClient {	
    private String url;
    
    public NetworkClient(){
    	System.out.println("생성자 호출, url = " + url);
    }
    // 의존관계 주입
    public void setUrl(String url){
    	this.url = url;
    }
    // 초기화 콜백 하고 싶은 method를 init() 메서드 내부에 넣기
    // -> 설정 파일 bean에서 초기화 콜백 method 명시
    public void init(){
    	System.out.println("DI까지 끝나면 호출한다는 method - InitiallizingBean");
    	connect(); // 초기화 콜백 하고 싶은 method
        call("초기화 연결 메시지"); // 초기화 콜백 하고 싶은 method
    }
	// 소멸전 콜백하고 싶은 method를 close() 메서드 내부에 넣기
    public void close(){
    	System.out.println("spring container 작업이 종료되면 소멸시키는 method - DisposableBean");
        disConnect(); // 소멸전 콜백 하고픈 method
    }
}

// 설정 파일
@Configuration
Static class LifeCycleConfig{
	
    // 설정 파일 bean에서 초기화 콜백, 종료 콜백 method 명시
    // 초기화 할 callback method명을 "init"처럼 string으로 넣어주면된다.
    @Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient(){
    	NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-spring.dev");
        return networkClient;
    }
}

 

  • @PostConstruct, @PreDestroy

제일 많이 사용하지만 외부 라이브러리에는 적용하지 못한다.

직접 bean으로 등록될 method에서 초기화 콜백, 소멸전 콜백 어노테션을 붙인다. = DI까지 완료한 후 초기화 콜백을 수행

 

public class NetworkClient {	
    private String url;
    
    public void setUrl(String url){ 
        this.url = url; 
    }
    public NetworkClient(){ 
        System.out.println("생성자 호출, url = " + url); 
    }
    // 초기화 콜백
    @PostConstruct
    public void init(){
    	connect();
        call("초기화 연결 메시지");
    }
	// 소멸전 콜백
    @PreDestroy
    public void close(){
        disConnect();
    }
    // 서비스 시작 ~ 서비스 종료 까지의 코드
    public void connect(){ System.out.println("connnect: " + url); }
    public void call(String message){ System.out.println("call: " + url + ", message = " + message); }    
    public void disconnect(){ System.out.println("close: " + url); }
}

// TEST
@Configuration
Static class LifeCycleConfig{
    @Bean
    public NetworkClient networkClient(){
    	NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-spring.dev");
        return networkClient;
    }
}

 

8. bean scope

  • singleton

  • prototype

  • web-scope
    • request
    • session
    • application

 

  • scope 등록 방식
// 자동 등록
@Component
@Scope("prototype")
public class HelloBean{}

// 수동 등록
@Bean
@Scope("prototype")
public class HelloBean{}

 

핵심

  1. prototype scope
  2. prototype scope + singleton
  3. request Scope

 

  • 2, 3 핵심의 문제는 prototype은 singleton과 엮여도 항상 새로운 객체가 생성되길 바라고 request Scope이 di로 들어가야 되는 경우 요청이 오기전에 주입 되야하므로 문법적 오류가 발생한다.
  • 해결
    • DI 대신, DL이란 개념을 이용한다. - 의존관계 조회(탐색) 수행
    • DL이란 가짜 객체를 일단 주입 하고 실제로 필요할 때 진짜 객체를 넣어주는 방식

 

8.1. DL 방식

  1. spring way : object provider
  2. java way : provider

 

  • spring way : object provider
@Scope("singleton")
static class ClientBean {

    private final ObjectProvider<PrototypeBean> prototypeBeanProvider;

    @Autowired
    public ClientBean(ObjectProvider<PrototypeBean> prototypeBeanProvider) {
        this.prototypeBeanProvider = prototypeBeanProvider;
    }

    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); // 내부에서 스프링 컨테이너를 통해서 해당 빈을 찾아서 반환
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}

 

  • java way : provider
# gradle
jakarta.inject:jakarta.inject-api:2.0.1
@Scope("singleton")
static class ClientBean {

    private final Provider<PrototypeBean> provider;

    @Autowired
    public ClientBean( Provider<PrototypeBean> provider) {
        this.provider = provider;
    }

    public int logic() {
        PrototypeBean prototypeBean = provider.get(); // 내부에서 스프링 컨테이너를 통해서 해당 빈을 찾아서 반환
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}

 

  • 참고 : web scope - request를 provider보다 더 편한걸로 사용하는 방식 - 필요시 사용