Spring Ioc, DI, 생성자주입방식 그리고 빈 스코프

devdo·2021년 12월 30일
0

Spring

목록 보기
5/11
post-thumbnail

IoC (Inversion of Control)

스프링에서는 일반적인 Java 객체를 new로 생성하여 개발자가 관리 하는 것이 아닌 Spring Container에 모두 맡긴다.

즉, 개발자가 main문이 있는 자바객체에서 생성하는 방식 -> 프레임워크, Ioc 컨테이너가 모두 객체를 관리하는 방식으로, 제어의 객체 관리의 권한이 넘어 갔음으로 제어의 역전 이라고 한다.

객체들의 의존관계 방향성만 따진다면, SOLID 원칙 중 DIP 원칙과 밀접한 관련을 갖는다. DIP 는 상위객체가 new 연산자를 쓰면서 하위 객체에 의존성을 갖게하는 기존의 방식과 달리, interface를 둠으로써 하위 객체가 오히려 상위 객체에 의존성을 갖게하는 방식을 말한다.


  • 개발자가 직접 생성하는 방향
    상위객체가 하위객체(구현클래스)에 의존한다.

  • 프레임워크가 객체를 생성하는 방향
    인터페이스(매개체)를 통해서 하위객체(Impl)가 상위객체에 의존하게 한다
    : DIP(의존관계 역전) 충족
    인터페이스의 구현클래스가 확장한다 할지라도 인터페이스에 의존하는 클라이언트 객체는 변화에 닫혀있게 된다.
    : OCP(개방-폐쇄) 충족



DI (Dependency Injection)

IoC의 기술, 의존성 주입이라고 한다. 정확하게 표현 의존관계 주입이다. 외부에서 주입하는 방식이란 뜻인데, 컨테이너가 관심사의 분리(생성과 사용의 분리)로 객체의 의존관계를 연결해주는 기술이라는 것이다.

보통, XML, Annotation 등 으로 구현된다.


▶ 관심사의 분리(생성과 사용의 분리)
ApplicationContext 객체가 생성하고
클라이언트 객체는 그 객체를 사용 해서 호출만 하면 된다.

▶ 의존성 주입 원칙
1) 상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 한다.
2) 상위, 하위 둘다 추상화에 의존해야 하며, 이때 추상화는 세부 사항에 의존하지 말아야 한다.


▶ DI 장점

  • 의존성으로부터 격리시켜 코드 테스트에 용이하다
  • DI를 통하여 불가능한 상황을 Mock과 같은 기술을 통하여, 안정적으로 테스트 가능하다.
  • 코드를 확장하거나 변경할 때 영향을 최소화한다. (추상화)
    - 느슨한 결합 -> 런타임시 의존관계가 결정됨.
  • 순환참조를 막을 수 있다.

참고) 느슨한 결합

  • 강한 결합
    객체 내부에서 다른 객체를 생성하는 것은 강한 결합도를 가지는 구조이다. A 클래스 내부에서 B 라는 객체를 직접 생성하고 있다면, B 객체를 C 객체로 바꾸고 싶은 경우에 A 클래스도 수정해야 하는 방식이기 때문에 강한 결합이다.
  • 느슨한 결합
    객체를 주입 받는다는 것은 외부에서 생성된 객체를 인터페이스를 통해서 넘겨받는 것이다. 이렇게 하면 결합도를 낮출 수 있고, 런타임시에 의존관계가 결정되기 때문에 유연한 구조를 가진다.

SOLID 원칙에서 O 에 해당하는 Open Closed Principle 을 지키기 위해서 디자인 패턴 중 전략패턴을 사용하게 되는데, 생성자 주입을 사용하게 되면 전략패턴을 사용하게 된다.


▶ 스프링 DI 구현

XML, Annotation(@Component, @bean, @Configuration, @Autowired, @Inject 등)으로 spring에서 관리하는 객체, @bean으로 싱글톤 객체로 등록시킨다.

  • @bean : 직접 bean으로 등록할 때 사용

  • @Configuration : 여러가지 bean을 등록할 때 사용

ex.

@Configuration
public class AppConfig {

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

▶ 사용자는 어떻게 bean 객체를 가져올 수 있나?

사실, 스프링에서는 ApplicationContext 환경에서 bean을 관리하는 것이다. bean 팩토리라고도 불린다.

실제로 개발자가 직접 접근할려면 beanFactory 기능을 상속 받은 ApplicationContext 객체(인터페이스)에서 getContext() , getBean() 메서드로 불러온다.


✔ 실제 ApplicationContext 객체

ApplicationContext context = ApplicationContextProvider.getContext();

MyClass myClass = context.getBean(myClass.class);

실제예시

public class MemberApp {
    public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
        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("new member = " + member.getName());
        System.out.println("find Member = " + findMember.getName());
    }
}

▶ 실제 많이 사용하는 방식은 @Autowired 방식(프로퍼티 생성방식)과 @RequiredargsConstructor(생성자 생성방식- 스프링부트4.0부터 밀고 있는 방식 )이다.


▶ 어노테이션 DI 방식의 주의점

생성자주입방식, @RequiredArgsConstructor 을 이용해서 편리하게 의존성 주입하는 방법을 알아봤다. 추가적으로 Lombok 어노테이션이(@Getter 혹은 @Setter 등) 사용할땐 편하지만, 단점도 있다는 것을 알아야 한다.

setter 메서드가 필요없는 필드에 대해서도 setter 메서드를 강제로 생성하게 되니, 필드 값이 변경될 위험이 생기게 된다.

이런 부분들은 Lombok을 사용하게될 경우 리팩토링이 힘들어지는 부분도 있으니 너무 무분별하게 사용하지 않는 것이 좋다.


생성자 주입을 사용하는 이유! 순환참조 방지

개발하다보면 여러 서비스들 간에 의존관계가 생기게 되는 경우가 있다. 서로서로 주거니 받거니 호출을 반복하면서 끊임없이 호출하다가 결국 StackOverflowError 를 발생시키고 죽는다 경우가 생기는데 이게 순환참조 문제이다.

이 순환참조의 문제는 실제 코드가 호출이 되기 전까지는 오류가 있다는 것을 알지 못한다는 것이다!

그런데 생성자 주입 방식에서는 이를 방지할 수 있게 해준다!

실제 생성자주입 방식을 사용할 때, 순환 참조가 생길시 나오는 오류!

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  courseServiceImpl defined in file [/Users/yaboong/.../CourseServiceImpl.class]
↑     ↓
|  studentServiceImpl defined in file [/Users/yaboong/.../StudentServiceImpl.class]
└─────┘

빈 생성시 아래와 같은 로직이 수행되면서 어떤 시점에 스프링이 그것을 캐치해서 순환참조라고 알려주는 것 같다.

new CourseServiceImpl(new StudentServiceImpl(new CourseServiceImpl(new ...)))

이처럼 생성자 주입을 사용하면 객체 간 순환참조를 하고 있는 경우에 스프링 애플리케이션이 구동되지 않는다.

그런데, 필드주입, 수정자 주입을 사용하면 순환참조를 하고 있는 부분에 대한 호출이 이루어질 경우 StackOverflowError 를 뱉기 때문에, 오류를 뱉을 수 밖에 없는 로직을 품고 애플리케이션이 구동되는 것이다!

@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
}

@Component
public class ServiceB {
    @Autowired
    private ServiceA serviceA;
}

생성자 주입

@Component
public class ServiceA {
    private final ServiceB serviceB;

    @Autowired
    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Component
public class ServiceB {
    private final ServiceA serviceA;

    @Autowired
    public ServiceB(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

반대로, 생성자주입은 컨테이너가 빈을 생성하는 시점에서 객체생성에 사이클관계가 생기고 이를 어플리케이션이 실행이 안되게 된다!

생성자 방식을 꼭 사용하자!


⭐ 그외 생성자 주입 사용하는 이유 정리

  • 순환 참조 방지

  • Spring에서 필드 주입(@Autowired)이나 수정자 주입(setter 주입)을 사용하면, 두 개 이상의 빈이 서로를 참조할 경우 순환 참조 문제가 발생할 수 있습니다.
    -> 하지만 생성자 주입을 사용하면 순환 참조가 감지되면 즉시 애플리케이션이 실행되지 않도록 방지할 수 있습니다!

  • 객체의 불변성(Immutable) 확보
    - 생성자 주입을 사용하면 final 키워드를 활용할 수 있어, 객체가 생성된 후 의존성이 변경되지 않도록 보장할 수 있습니다.

  • 테스트 코드 작성 용이
    - 생성자 주입을 사용하면 Mock 객체를 쉽게 주입할 수 있어 단위 테스트가 편리합니다.


그렇다면 순환참조 문제 발생시, 대처법은?

개발시, 설계가 잘못돼어서 순환참조가 발생하는 케이스가 가장 클 것이다!

순환 참조가 발생하는 구조 자체를 변경하는 것이 가장 좋은 방법입이다!

두 개의 클래스가 서로를 참조하지 않도록 ⭐ 중간 인터페이스 ⭐ 나 이벤트 기반 구조를 활용할 수 있습니다.


스프링 빈의 이벤트 라이프사이클

스프링 컨테이너 생성 스프링 빈 생성 의존관계 주입 초기화 콜백 사용 소멸전 콜백 스프링
종료

스프링은 다양한 방식으로 생명주기 콜백을 지원한다.

스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원한다.

1) 인터페이스(InitializingBean, DisposableBean)
2) 설정 정보에 초기화 메서드, 종료 메서드 지정
3) ⭐ @PostConstruct, @PreDestroy 애노테이션 지원 (권장)

@PostConstruct, @PreDestroy 애노테이션 특징

  • 최신 스프링에서 가장 권장하는 방법이다.

  • 애노테이션 하나만 붙이면 되므로 매우 편리하다.

  • 패키지를 잘 보면 javax.annotation.PostConstruct 이다. 스프링에 종속적인 기술이 아니라 JSR-250 라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.

  • 컴포넌트 스캔과 잘 어울린다.

  • 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용하자.

📌 정리

  • @PostConstruct, @PreDestroy 애노테이션을 사용하자
    코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야 하면
  • @Bean 의 initMethod , destroyMethod
    를 사용하자.

스프링 빈 스코프

빈 스코프란?

지금까지 우리는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때
까지 유지된다고 학습했다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻한다.

스프링은 다음과 같은 다양한 스코프를 지원한다.

  • 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
  • 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는
    매우 짧은 범위의 스코프이다.

  • 웹 관련 스코프
    * request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.

    • session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.
    • application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.

⭐ 결론!
이들 형태의 Bean들은 사용할 일이 거의 없다.
지금까지 Controller, Service, Repository 형태로의 상태를 가지지 않도록(stateless) 설계된 방식은 싱글톤 빈 방식으로 충분하다!



참고

profile
배운 것을 기록합니다.

0개의 댓글