Spring Framework 시작하기(2)
이번 시간에는 의존관계 주입, Bean들이 자동으로 IoC Container에 주입이 되는 컴포넌트 스캔, 빈 스코핑에 대하여 알아보겠다.
Dependency Injection
지난 시간에는 IoC Container에 대하여 배웠다.
- IoC는 다양한 방법으로 만들어질 수 있다.
- 전략 패턴
- 서비스 로케이터 패턴
- 팩토리 패턴
- 의존관계 주입 패턴
- 의존성을 생성자로부터 전달(주입)받는다.
- Circular dependencies가 생길 때 잘못 등록되었다고 한다.
Circular dependencies
: 순환 의존 관계
- A → B를 참조하고 B → A를 참조할 경우
//CircularDepTester.java
import org.springframework.context.annotationConfigApplicationContext;
class A {
private final B b;
A(B b) {
this.b = b;
}
}
class B {
private final A a;
B(A a) {
this.a = a;
}
}
@Configuration
class CircularConfig {
@Bean
public A a(B b) {
return new A(b);
}
@Bean
public B b(A a) {
return new B(a);
}
}
public class CircularDepTester{
public static void main(String[] args) {
var annotationConfigApplicationContext = new AnnotationConfigApplicationContext(CircularConfig.class);
}
}
<실행결과>
- Is there an unresolvable circular reference?
- BeanCurrentlyInCreationException
[주의] Circular dependencies를 만들 경우 Bean 생성이 되지 않는다!
컴포넌트 스캔
: 스프링이 직접 클래스를 검색해서 Bean을 등록해주는 기능이다.
- 우리가 일일이 빈을 등록하지 않고, 스프링이 검색하여 자동으로 빈으로
Q. 스프링은 어떻게 빈을 찾는 것일까?
: Stereotype Annotation을 이용한다.
Stereo type
: UML에서 용어가 왔다.
- UML 컴포넌트들을 확장시켜주는 도구
스프링에서도 Bean들을 동일시하게 생각하지 않는다.
용도에 맞게 분류를 시켜준다.
자세한 설명을 보기 위해서는 아래의 사이트를 참고해보자.
https://incheol-jung.gitbook.io/docs/q-and-a/spring/stereo-type
Stereo Type(스테레오 타입) - Incheol's TECH BLOG
Stereo Type이 범용적으로 많이 사용하게 된 시키는 Spring 2.5 부터 였다. 그 이전까지는 xml 파일에 bean을 등록하여 관리 하였다. 그러나 모든 bean들을 xml 파일로 관리 하다보니 다른 보일러플레이트
incheol-jung.gitbook.io
@Service
OrderService, VoucherSerive에 Stereotype Annotation을 적용해보자.
- @Service 어노테이션을 추가해주면 된다.
//OrderService.java
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final VoucherService voucherService;
private final OrderRepository orderRepository;
public OrderService(VoucherService voucherService, OrderRepository orderRepository) {
this.voucherService = voucherService;
this.orderRepository = orderRepository;
}
public Order createOrder(UUID customerId, List<OrderItem> orderItems) {
var order = new Order(UUID.randomUUID(), customerId, orderItems);
orderRepository.insert(order);
return order;
}
public Order createOrder(UUID customerId, List<OrderItem> orderItems, UUID voucherId) {
var voucher = voucherService.getVoucher(voucherId);
var order = new Order(UUID.randomUUID(), customerId, orderItems);
orderRepository.insert(order);
voucherService.useVoucher(voucher);
return order;
}
}
// VoucherService.java
import org.springframework.sterotype.Service;
@Service
public class VoucherService {
private final VoucherRepository voucherRepository;
public VoucherService(VoucherRepository voucherRepository) {
this.voucherRepository = voucherRepository;
}
public Voucher getVoucher(UUID voucherId) {
return voucherRepository
.findById(VoucherID)
.orElseThrow(() -> RuntimeException(MessageFormat,format("can't find voucher id {0}", voucherId)));
}
public void useVoucher(Voucher voucher) {
}
}
아래의 [더보기]를 누르면 수정된 AppConfiguration을 볼 수 있다.
@Bean 처리한 OrderService, VoucherSerive를 지워도 동작한다.
@Configuration
@ComponentScan
public class AppConfiguration {
@Bean
VoucherRepository voucherRepository() {
return new VoucherRepository() {
@Override
public Optinal<Voucher> findById(UUID voucher) {
return Optinal.empty();
}
};
}
@Bean
OrderRepository orderRepository() {
return new orderRepository() {
@Override
public void insert(Order order){
return Optinal.empty();
}
};
}
// OrderService, VoucherService를 지워도 동작한다.
}
파일 패키지 기준으로 쭉 찾는다.
@Repository
Repository 는 인터페이스가 아니라 구현체에다가 어노테이션을 추가한다.
// MemoryVoucherRepository.java
@Repository
public class MemoryVoucherRepository implements VoucherRepository {
private final Map<UUID, Voucher> storage = new ConcurrentHashMap<>();
@Override
public Optional<Voucher> findById(UUID voucherId) {
// 만약 null -> EMPTY가 반환
return Optional.ofNullable(storage.get(voucherId));
}
@Override
public Voucher insert(Voucher voucher) {
storage.put(voucher.getVoucherId(), voucher);
return voucher;
}
}
// VoucherRepository.java
public interface VoucherRepository {
Optional<Voucher> findById(UUID voucherId);
Voucher insert(Voucher voucher);
}
order도 바꿔보자.
// MemoryOrderRepository.java
@Repository
public class MemoryOrderRepository implements OrderRepository {
private final Map<UUID, Order> storage = new ConcurrentHashMap<>();
@Override
public Order insert(Order order) {
storage.put(order.getOrderId(), order);
return order;
}
}
// OrderRepository.java
public interface OrderRepository {
Order insert(Order order);
}
아래의 [더보기]를 누르면 수정된 AppConfiguration을 볼 수 있다.
@Bean 들이 다 없어졌다.
@Configuration
@ComponentScan
public class AppConfiguration {
// orderRepository, voucherRepository를 지워도 동작한다.
// OrderService, VoucherService를 지워도 동작한다.
}
Tester로 가서 확인해보자.
// OrderTester.java
public class OrderTester {
public static void main(String []args) {
var applicationContext = new AnnotationConfigapplicationContext(AppConfiguration.class);
var customerId = UUID.randomUUID();
var voucherRepository = applicationContext.getBean(VoucherRepository.class);
var voucher = voucherRepository.insert(new FixedAmountVoucher(UUID.randomUUID(), 10L));
var orderService = applicationContext.getBean(orderService.class);
var orderItems = new ArrayList<orderItem>() {{
add(new OrderItem(UUID.randomUUID(), 100L, 1));
}};
var order = orderService.createOrder(customerId, orderItems, voucher.getVoucherId());
Assert.isTrue(order.totalAmount() == 90L, MessageFormat.format("{0} is not 90L", order.totalAmount()));
}
}
- OrderService의 변경이 없었다.
- ConponentScan을 사용해서 범위를 조절할 수 있다.
(1) base package 조절한다.
@ComponentScan(basePackages=("org.---.kdt.xxx00", "org.---.kdt.xxx01"))
- 원하는 것만 Scan 할 수 있다.
- 문자열 띄어쓰기나 ,(콤마)를 통해서도 가능하지만 비추(오타)
(2) base package class 조절한다.
@ComponentScan(basePackageClasses={Order.class, Voucher.class})
// class가 속한 package 기준으로 찾는다.
(3) excludeFilters 를 통해서 뺄 수도 있다.
@ComponentScan(excludeFilters={@ComponentScan.Filter(type = FilterType.ASSIGNBLE_TYPE, value=xxx.class)})
// Bean으로 등록된 것을 제거
4. Autowired
@Autowired를 이용한 의존관계 자동 주입에 대하여 알아보자.
- 스프링은 Application Context에 등록된 Bean을 코드에서 직접 주입하지 않는다.
- 자동으로 의존관계를 형성해주는 기능이 있다.
// VoucherService.java
import org.springframework.sterotype.Service;
@Service
public class VoucherService {
@Autowired
private final VoucherRepository voucherRepository;
// public VoucherService(VoucherRepository voucherRepository) {
// this.voucherRepository = voucherRepository;
// }
public Voucher getVoucher(UUID voucherId) {
return voucherRepository
.findById(VoucherID)
.orElseThrow(() -> RuntimeException(MessageFormat,format("can't find voucher id {0}", voucherId)));
}
public void useVoucher(Voucher voucher) {
}
}
- VoucherRepository가 IoC Container에 의해서 자동으로 주입이 된다.
- 코드가 상당히 줄어든다.
- field에다가 autowired를 줄 수 있고, setter를 통해 줄 수도 있다.
// VoucherService.java
import org.springframework.sterotype.Service;
@Service
public class VoucherService {
private final VoucherRepository voucherRepository;
// public VoucherService(VoucherRepository voucherRepository) {
// this.voucherRepository = voucherRepository;
// }
public Voucher getVoucher(UUID voucherId) {
return voucherRepository
.findById(VoucherID)
.orElseThrow(() -> RuntimeException(MessageFormat,format("can't find voucher id {0}", voucherId)));
}
public void useVoucher(Voucher voucher) {
}
@Autowired
public void setVoucherRepository(VoucherRepository voucherRepository) {
this.voucherRepository = voucherRepository
}
}
- 원래 코드에서는 생성자 주입을 통해서 자동으로 의존관계 주입이 된 것이다. default
- 만약 생성자가 두 개라면,
- 자동으로 주입이 되는 생성자에게 @을 달아준다.
스프링에서는 생성자 주입을 옹호한다.
- 초기화시에 필요한 모든 의존관계가 형성되기 때문에 안전하다.
나중에 참조해야 될 필드가 없어서 생기는 null pointer exception (x)
Optional type으로 정의한다.
- 잘못된 패턴을 찾을 수 있게 도와준다.
많은 파라미터를 갖고 있는 클래스는 수많은 책임을 가지고 있다.
- 테스트를 쉽게 해준다.
- 불편성을 확보한다.
final -> 한번 만든 불변 관계가 변경되지 않게 도와준다.
- @Primary Annotation을 달아주면 똑같은 게 발생했을 때 우선순위를 제어해준다.
- @Qualifier("memory") // "jdbc" 를 하고 Bean을 명시해준다.
var voucherRepository = BeanFactoryAnnotationUtils.qulifiedBeanOfType(applicationContext.getBeanFactory(), voucherRepository.class, "memory");
- 쓰는 쪽에서는 고민을 안 하는 것이 낫다. @Primary
- 템플릿 다수, 서버 다수.. 이러면 @Qualifier를 활용하는 것이 좋다.
(A 서버 접속용, B 서버 접속용.. 등등)
- 복수 개의 Bean 설정을 용도에 맞춰서 설정해야 한다.
5. Bean Scope
빈이 어떤 범위로 만들어지는가. -> 스프링에게 어떻게 객체를 만들어야 하는가.
- 하나의 bean defination에 의해서 여러 개의 객체가 만들어질 수도 있다.
- singleton scope: 단 하나의 객체가 만들어지는 것 (Default)
- application context에서 bean을 불러오면 매번 같은 객체에서 불러온다.
매번 새로운 객체를 생성하고 싶으면?
: 프로토타입 스콥을 설정하면 된다!
// MemoryVoucherRepository.java
@Repository
@Qualifier("memory")
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) // SCORE_PROTOTYPE
public class MemoryVoucherRepository implements VoucherRepository {
private final Map<UUID, Voucher> storage = new ConcurrentHashMap<>();
@Override
public Optional<Voucher> findById(UUID voucherId) {
// 만약 null -> EMPTY가 반환
return Optional.ofNullable(storage.get(voucherId));
}
@Override
public Voucher insert(Voucher voucher) {
storage.put(voucher.getVoucherId(), voucher);
return voucher;
}
}
- 정말 필요한 게 아니라면 싱글톤으로 작성하는 것이 좋다.
6. Lifes Cycle
스프링 Application Context는 객체 생성과 소멸, 즉 생명주기를 관리한다.
- 스프링 Container 조차도 생명주기를 가진다.
- 소멸 : Application Context 에서 close()라는 메소드가 있다.
applicationContext.close();
- Container에 등록된 모든 Bean 이 소멸하게 된다.
- 소멸에 대한 callback이 동작한다.
Bean 생성 생명주기 콜백
1. @PostConstruct Annotation이 적용된 메소드 호출
2. Bean이 InitializingBean 인터페이스 구현시 afterPropertiesSet 호출
3. @Bean Annotation의 initMethod 에 설정한 메소드 호출
Bean 소멸 생명주기 콜백
1. @PreDestory Annotation이 적용된 메소드 호출
2. Bean이 DisposableBean 인터페이스 구현시 destroy 호출
3. @Bean Annotation의 destroyMethod 에 설정한 메소드 호출
// MemoryVoucherRepository.java
@Repository
@Qualifier("memory")
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) // SCORE_PROTOTYPE
public class MemoryVoucherRepository implements VoucherRepository, InitializingBean {
private final Map<UUID, Voucher> storage = new ConcurrentHashMap<>();
@Override
public Optional<Voucher> findById(UUID voucherId) {
// 만약 null -> EMPTY가 반환
return Optional.ofNullable(storage.get(voucherId));
}
@Override
public Voucher insert(Voucher voucher) {
storage.put(voucher.getVoucherId(), voucher);
return voucher;
}
@PostConstruct
public void postConstruct() {
}
@Override
public void afterPropertiesSet() throws Exception {
}
}
- PostConstruct -> afterPropertiesSet 순으로 호출된다.
소멸시에 콜백도 한번 보자.
// MemoryVoucherRepository.java
@Repository
@Qualifier("memory")
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) // SCORE_PROTOTYPE
public class MemoryVoucherRepository implements VoucherRepository, InitializingBean, DisposableBean {
private final Map<UUID, Voucher> storage = new ConcurrentHashMap<>();
@Override
public Optional<Voucher> findById(UUID voucherId) {
// 만약 null -> EMPTY가 반환
return Optional.ofNullable(storage.get(voucherId));
}
@Override
public Voucher insert(Voucher voucher) {
storage.put(voucher.getVoucherId(), voucher);
return voucher;
}
// 생성시
@PostConstruct
public void postConstruct() {}
@Override
public void afterPropertiesSet() throws Exception {}
// 소멸시
@PreDestroy
public void preDestroy() {}
@Override
public void destroy() throws Exception {}
}
- preDestroy -> destroy
'2023 활동 - 4학년 > [1월 ~ 4월] sw 아카데미 백엔드 과정' 카테고리의 다른 글
[2023.02.28 / CNU SW 아카데미] SpringBasic Part1 D-19 (0) | 2023.02.28 |
---|---|
[2023.02.28 / CNU SW 아카데미] 40일차 회고록 (2) | 2023.02.28 |
[2023.02.27 / CNU SW 아카데미] 39일차 회고록 (0) | 2023.02.27 |
[2023.02.24 / CNU SW 아카데미] 38회차 회고록 (2) | 2023.02.26 |
[2023.02.23 / CNU SW 아카데미] SpringBasic Part1 - D17 (0) | 2023.02.23 |