본문 바로가기
2023 활동 - 4학년/[1월 ~ 4월] sw 아카데미 백엔드 과정

[2023.02.27 / SW CNU 아카데미] SpringBasic Part1 D-18

by 은행장 노씨 2023. 2. 27.

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들을 동일시하게 생각하지 않는다. 

용도에 맞게 분류를 시켜준다. 

 

스프링의 대표적인&nbsp;Stereo type

자세한 설명을 보기 위해서는 아래의 사이트를 참고해보자. 

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