1. DI
1-1. DI란? (Dependency Injection)
DI는 IoC라는 원칙을 구현하기 위해서 사용되는 방법 중 하나로 의존성 주입을 말한다.
지금까지는 우리가 직접 설정 파일을 통해 의존성 주입을 했는데 지금부터는 스프링에서 지원하는 DI를 위한 내용을 알아보자.
1-2. 스프링 컨테이너(Spring Container)
스프링 컨테이너는 내부에 존재하는 애플리케이션 빈의 생성, 관리 제거 등 생명 주기를 관리한다.
스프링 컨테이너는 XML, 애너테이션 기반의 자바 설정 클래스로 만들 수 있으며 서로 다른 빈을 연결해 애플리케이션 빈을 연결하는 역할을 한다.
ApplicationContext라는 인터페이스를 스프링 컨테이너라고 한다.
정확히 말하면 BeanFactory와 BeanFactory의 하위 클래스인 ApplicationContext로 구분해서 이야기하지만 BeanFactory를 직접 사용하는 경우는 거의 없기 때문에 일반적으로 ApplicationContext를 스프링 컨테이너라고 하는 것이다.
그럼 스프링 컨테이너는 왜 사용하는 걸까?
→ 객체 간의 결합도를 낮추기 위해 사용한다.
구체적으로 얘기하자면 기존의 방식(높은 결합도)은 수정과 유지보수에 좋지 않다.
하지만 스프링 컨테이너를 사용하면 구현 클래스에 있는 의존을 제거하고 인터페이스에만 의존하도록 설계할 수 있다.
다음은 스프링 컨테이너의 생성 과정을 알아보자.
스프링 컨테이너는 @Configuration이 붙은 클래스를 설정 정보로 사용하고, @Bean이 적힌 메서드를 모두 호출해 반환된 객체를 스프링 컨테이너에 등록한다.
이 때 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다.
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(DependencyConfig.class);
1-3. 빈(Bean)
빈(Bean)이란 스프링 컨테이너가 관리하는 자바 객체를 의미한다.
Bean은 클래스의 등록 정보, 게터/세터 메서드를 포함하며 컨테이너에 사용되는 설정 메타데이터로 생성된다.
💡 설정 메타데이터는 XML 또는 자바 애너테이션, 자바 코드로 표현하며 컨테이너의 명령과 인스턴스화, 설정, 조립할 객체를 정의한다.
ApplicationContext 인터페이스는 bean을 가져오는 몇 가지 방법이 있지만 스프링은 DI를 통해 Bean 객체를 자동으로 주입하는 방식을 권장한다.
1-4. 빈 스코프(Bean Scope)
빈 스코프는 빈 객체의 생성과 소멸에 대한 범위를 정의하는 것이다.
특정 Bean 정의에서 생성된 개체에 연결할 다양한 의존성 및 구성 값 뿐만 아니라 생성된 개체의 범위도 제어할 수 있다.
스프링 프레임워크는 6개의 범위를 지원하며 그 중 4개는 ApplicationContext를 통해서만 사용할 수 있다.
이 중 싱글톤 스코프에 대해 자세히 알아보자.
싱글톤(singleton) 스코프란 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
스프링 컨테이너의 시작과 함께 생성돼서 스프링 컨테이너가 종료될 때까지 유지된다.
//SingletonService 클래스
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
return instance;
}
private SingletonService() {}
}
//SingletonTest 클래스
public class SingletonTest {
static SingletonService singletonService1 = SingletonService.getInstance();
static SingletonService singletonService2 = SingletonService.getInstance();
public static void main(String[] args) {
System.out.println("singletonService1 : " + singletonService1);
System.out.println("singletonService2 : " + singletonService2);
}
}
//실행 결과
singletonService1 : com.codestates.section2week4.singleton.SingletonService**@85ede7b**
singletonService2 : com.codestates.section2week4.singleton.SingletonService**@85ede7b**
먼저 SingletonService 클래스의 static 영역에 인스턴스를 1개 생성하고, 외부에서 new로 객체를 생성하는 것을 막기 위해 생성자를 private으로 선언한다.
객체 인스턴스가 필요한 경우 getInstance() 메서드를 통해 조회만 할 수 있게 했다.
실행 결과를 보면 singletonService1 객체와 singletonService2 객체가 같은 값을 가지는 것을 알 수 있다.
💡 싱글톤 패턴은 왜 사용하는 걸까?
고정된 메모리 영역을 얻으면서 한 번의 new로 인스턴스를 사용하기 때문에 메모리 낭비를 방지할 수 있고, 싱글톤으로 만들어진 클래스의 인스턴스는 전역 인스턴스이기 때문에 다른 클래스의 인스턴스들이 데이터를 공유하기 쉽다는 장점이 있다.
공통된 객체를 여러 개 생성해서 사용해야 하는 상황에서 많이 사용한다.
하지만 싱글톤 패턴을 사용할 때도 주의해야 할 점이 있다.
싱글톤 인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시킬 경우 다른 클래스의 인스턴스들 간에 결합도가 높아져 “개방-폐쇄 원칙”을 위배하게 된다.
또한 멀티 쓰레드 환경에서 여러 쓰레드에 의해 객체 속성이 바뀔 수 있다.
예를 들어, A 쓰레드에서 속성 값을 x로 바꾸고 출력하는 과정에서 B 쓰레드가 속성 값을 y로 바꾸면 쓰레드 A에서 예상하지 못한 값이 나올 수 있다.
이러한 싱글톤 패턴의 문제점을 싱글톤 컨테이너가 해결해준다.
// DependencyConfig 클래스
@Configuration
public class DependencyConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemberRepository();
}
@Bean
public CoffeeService coffeeService() {
return new CoffeeService(coffeeRepository());
}
@Bean
public CoffeeRepository coffeeRepository() {
return new CoffeeRepository();
}
}
//singletonTest 클래스
public class SingletonTest {
static AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(DependencyConfig.class);
static MemberService memberService1 = ac.getBean("memberService", MemberService.class);
static MemberService memberService2 = ac.getBean("memberService", MemberService.class);
public static void main(String[] args) {
System.out.println("memberService1 : " + memberService1);
System.out.println("memberService2 : " + memberService2);
}
}
스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도 싱글톤으로 객체들을 관리한다.
즉, 싱글톤 컨테이너의 역할을 하는 것이다.
위의 코드처럼 싱글톤 컨테이너가 적용되면 많은 사용자가 각각 요청을 할 때마다 하나의 인스턴스를 공유해서 사용함으로써 효율적으로 재사용이 가능해진다.
하지만 싱글톤 컨테이너를 사용하더라도 하나의 객체 인스턴스를 공유하는 방식은 변함이 없기 때문에 해당 객체는 stateless 상태로 설계해야 한다.
💡 상태를 유지하지 않는 객체를 설계하는 방법
- 특정 클라이언트에 의존적인 필드가 있으면 안 된다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안 된다.
- 가급적 읽기만 가능해야 한다.
1-5. Java 기반 컨테이너(Container) 설정
//DependencyConfig 클래스
@Configuration
public class DependencyConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
//XML 설정 방식
<beans>
<bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>
먼저 AnnotationConfigApplicationContext를 사용하여 스프링 컨테이너를 인스턴스화하는 방법이 있다.
ApplicationContext 구현은 아래와 같은 애너테이션이 달린 클래스로 파라미터를 전달 받는다.
- @Configuration 클래스 : @Configuration클래스 자체가 Bean 정의로 등록되고 클래스 내에서 선언된 모든 @Bean 메서드도 Bean 정의로 등록된다.
- @Component 클래스, JSR-330 메타데이터 : Bean 정의로 등록되며 필요한 경우 해당 클래스 내에서 @Autowired 또는 @Inject와 같은 DI 메타데이터가 사용되는 것으로 가정한다.
//@Configuration 클래스를 입력으로 사용
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(DependencyConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
//@Component 또는 JSR-330 주석이 달린 클래스를 생성자에 입력으로 사용
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
다음은 Java 코드에서 애노테이션을 사용해서 스프링 컨테이너를 구성하는 방법이다.
- @Import 애노테이션 XML 파일 내에서 요소가 사용되는 것처럼 구성을 모듈화하는데 사용하며 다른 구성 클래스에서 @Bean definitions를 가져올 수 있다.
@Configuration
public class DependencyConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(DependencyConfigA.class)
public class DependencyConfigB {
@Bean
public B b() {
return new B();
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(DependencyConfigB.class);
// now both beans A and B will be available...
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}
main()에서 DependencyConfigB 클래스만 제공했지만 A 클래스와 B 클래스 둘 다 사용이 가능하다.
1-6. 컴포넌트 스캔(Component Scan)
지금까지는 스프링 빈을 등록하기 위해 @Bean 애노테이션을 붙이거나 <bean> XML 태그를 붙여서 직접 작성했지만 만약 사용해야 할 스프링 빈의 수가 늘어난다면 다양한 문제가 발생할 수 있다.
스프링에서는 이런 문제를 해결하기 위해 설정 정보 없이 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
@ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록해주기 때문에 설정 정보에 붙이기만 하면 된다.
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
@Configuration
@ComponentScan
public class AutoDependencyConfig {
}
기존에 사용했던 DependencyConfig와 비교한다면 @Bean으로 등록한 클래스를 볼 수 없다는 차이점이 있다. 만약 확인하고자 한다면 ApplicationContext에서 getBeanDefinitionNames() 메서드를 호출하여 등록된 모든 Bean의 이름을 조회할 수 있다.
컴포넌트 스캔을 사용하면 @Configuration이 붙은 설정 정보도 자동으로 등록된다.
@Component 애노테이션이 부여된 클래스를 탐색하여 등록한다고 했는데 그럼 어디서부터 탐색을 하는 걸까?
먼저 속성을 이용해 탐색위치를 지정할 수 있다.
@ComponentScan(basePackages = "hello.core")
이렇게 지정한 패키지를 포함해 하위 패키지를 모두 탐색하는 것이고 시작 위치를 여러 개 두는 것도 가능하다.
만약 따로 탐색 위치를 지정하지 않는다면 @ComponentScan 애노테이션이 존재하는 현재 패키지가 시작 위치가 된다.
그래서 설정 정보 클래스의 위치를 프로젝트 최상단에 두고 패키지 위치는 지정하지 않는 방법이 가장 편할 수 있다.
간혹 @Component 애노테이션을 붙이지 않았는데도 스프링빈에 등록돼서 사용이 가능한 경우가 있다.
- @Controller: 해당 애노테이션을 붙이면 스프링 MVC 컨트롤러로 인식된다.
- @Service: 스프링 비즈니스 로직에서 사용하며 특별한 처리는 하지 않고 이 애노테이션이 부여된 클래스에 핵심 비즈니스 로직이 들어있다고 인식하도록 해주는 역할이다.
- @Repository: 스프링 데이터 접근 계층에서 사용하며 데이터 계층의 예외를 스프링 예외로 변환해준다.
- @Configuration: 스프링 설정 정보에서 사용하며 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.
1-7. 다양한 의존 관계 주입 방법
스프링 DI에서 할 수 있는 의존 관계 주입은 4가지가 있다.
1. 생성자 주입
생성자에 @Autowired를 하면 스프링 컨테이너에 @Component로 등록된 빈에서 생성자에 필요한 빈들을 주입한다.
- 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다.
- 불변과 필수 의존 관계에 사용된다.
- 생성자가 1개만 존재하는 경우 @Autowired를 생략해도 자동으로 주입된다.
- NullPointerException을 방지할 수 있다.
- 주입받을 필드를 final로 선언이 가능하다.
@Component
public class CoffeeService {
private final MemberRepository memberRepository;
private final CoffeeRepository coffeeRepository;
@Autowired
public CoffeeService(MemberRepository memberRepository, CoffeeRepository coffeeRepository) {
this.memberRepository = memberRepository;
this.coffeeRepository = coffeeRepository;
}
}
2. 수정자 주입(setter 주입)
setter메서드를 통해 의존 관계를 주입한다.
- 선택과 변경 가능성이 있는 의존 관계에 사용된다.
- 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.
- 생성자 대신 set필드명 메서드를 생성하여 의존 관계를 주입한다.
- 수정자의 경우 @Autowired를 입력하지 않으면 실행되지 않는다.
@Component
public class CoffeeService {
private MemberRepository memberRepository;
private CoffeeRepository coffeeRepository;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setCoffeeRepository(CoffeeRepository coffeeRepository) {
this.coffeeRepository = coffeeRepository;
}
}
3. 필드 주입
필드에 @Autowired 붙여서 바로 주입한다.
- 외부에서 변경이 불가능하여 테스트하기 힘들다는 단점이 있다.
- DI 프레임워크가 없으면 아무것도 할 수 없다.
- 실제 코드와 상관 없는 특정 테스트를 하고 싶을 때 사용할 수 있다.
- 정상적으로 작동되게 하려면 결국 setter가 필요하게 돼서 수정자 주입을 사용하는 게 더 편리하다.
@Component
public class CoffeeService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private CoffeeRepository coffeeRepository;
}
4. 일반 메서드 주입
일반 메서드를 통해 주입받을 수 있다. 생성자처럼 한 번에 여러 필드를 주입 받을 수도 있는데 일반적으로 사용되지 않는다.
과거에는 수정자, 필드 주입을 많이 사용했지만, 아래 이유로 최근에는 대부분 생성자 주입 사용을 권장한다.
- 불변 의존 관계 주입은 최초 1회 주입 후 해당 의존관계가 변경될 일이 없지만 수정자 주입의 경우 setter를 public으로 열어두어 변경이 가능하기 때문에 적합하지 않다.
- 누락 수정자 의존 관계 혹은 필드 의존 관계인 경우 @Autowired 애노테이션이 스프링 프레임워크 내에서 문제가 있으면 오류를 발생하지만, 순수 자바로 짤 경우 실행은 되는 대신 NullPointException이 발생한다.
- final 키워드 사용 가능 생성자 주입을 제외한 나머지 주입 방식은 생성자 이후에 호출되는 형태이므로 final 키워드를 사용할 수 없다.
- 순환 참조 개발을 하다 보면 여러 컴포넌트 간에 의존성이 생기게 된다. 필드 주입과 수정자 주입은 빈이 생성된 후에 참조를 하기 때문에 문제가 있어도 정상적으로 구동이 되고, 실제 코드가 호출될 때까지 문제를 알 수 없다. 생성자 주입의 경우 BeanCurrentlyInCreationException이 발생한다.
'CodeStates_BE_44 > TIL' 카테고리의 다른 글
[Spring] Spring Framework (0) | 2023.03.31 |
---|---|
[DB] 데이터베이스 설계 (0) | 2023.03.29 |
[DB] SQL, NoSQL (0) | 2023.03.28 |
[네트워크] HTTP (0) | 2023.03.27 |
[네트워크] 웹 애플리케이션 작동 원리 (0) | 2023.03.24 |