일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- Today
- Total
- 도커
- 스프링 Configuration
- DI
- 객체지향
- Servlet Filter
- 스프링 싱글톤
- 의존관계 주입
- UsernamePasswordAuthenticationFilter
- 롬복 Qualifier
- 생성자 주입
- 스프링 빈 조회
- RequiredArgsConstructor
- 스프링 컨테이너
- 빈 중복 오류
- 라즈베리파이
- 싱글톤 컨테이너
- docker
- autowired
- ComponentScan
- 스프링
- 스프링 빈
- Spring interceptor
- HandlerMethodArgumentResolver
- 라즈베리파이4
- beandefinition
- qualifier
- springsecurity
- Autowired 옵션
- Spring
- DI컨테이너
그날그날 공부기록
스프링 DI 이해하기 본문
스프링의 원리를 이해하기 위해 스프링을 사용하지 않고 자바로 구현해보았다.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
고객 정보와 상품의 정보를 받아 주문 객체를 만드는 클래스이다.
할인 정책은 변경 가능하도록 DiscountPolicy라는 인터페이스를 만들고,
그 일정한 금액을 할인하는 FixDiscountPolicy, 상품 금액의 비율로 할인하는 RateDiscountPolicy를 구현체로 만들었다.
얼핏 보면 OrderServiceImpl은 DiscountPolicy 인터페이스에만 의존하는 것 같지만 구현체까지 모두 의존하고 있는 형태이다. → 추상화에만 의존해야 하지만 그렇지 않아 DIP위반이다~
구현체에도 의존하여 DIP를 위반하는 코드이기 때문에 할인 정책을 Fix → Rate로 변경할 시 클라이언트인 OrderServiceImpl의 소스코드를 변경해야 한다. → OCP를 위반해버린다!
해결방법
클라이언트가 추상화에만 의존하도록 하면 된다.
아래의 클라이언트 코드는 구현체에 의존하지 않고 인터페이스에만 의존한다. → DIP를 위반하지 않는다. (우선 DiscountPolicy에 대해서만 생각)
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
private DiscountPolicy discountPolicy;
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
하지만 할인 정책의 구현체가 없으므로 코드가 실행되지 않는다. → 누군가 DiscountPolicy의 구현체를 생성하고 주입해주어야 한다.
관심사의 분리
김영한 님이 설명하신 배역(인터페이스)과 배우(구현체)의 비유가 이해가 잘 되었다.
- 배우는 자신의 역할인 배역에만 집중해야 한다.
- 같은 배역을 맡은 배우라면 항상 같은 역할을 해야 한다. → 어떤 구현체든 인터페이스를 따라야 함.
제일 처음 나온 DIP, OCP를 위반하는 코드 역시 OrderService의 구현체이다.
현제 주문 객체를 만드는 것에만 집중해야 할 OrderServiceImpl 구현체가 할인 정책까지 스스로 결정하고 있다. → 비유하자면 배역에만 집중해야 하는 배우가 다른 배우까지 섭외하고 있다고 볼 수 있다.
이를 해결하려면 배우가 배역에만 집중할 수 있도록 배우를 섭외하는 사람인 기획자를 만들어야 한다.
즉, DiscountPolicy의 구현체를 생성하고 주입해주는 별도의 클래스를 생성해야 한다.
AppConfig 생성
이 전 코드는 MemberRepository의 구현체도 의존했지만 아래의 코드처럼 생성자를 통해 별도의 클래스를 통해 구현체를 주입받도록 했다. 이제 OrderServiceImpl은 완전히 구현체에 의존하지 않고, 인터페이스에만 의존한다.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
앞에서 말했던 별도의 클래스인 AppConfig 클래스는 클라이언트에게 구현체를 생성하여 주입해준다.
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new RateDiscountPolicy());
}
}
- DIP 준수: OrderServiceImpl은 의존관계에 대한 고민은 AppConfig에게 맡기고 자신의 역할에만 집중하면 된다.
- 관심사의 분리: 객체를 생성하고 연결하는 역할과 실행하는 역할이 분리되었다
클라이언트의 입장에서 보면 의존관계를 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) | 의존관계 주입 | 의존성 주입이라고 한다.
AppConfig 리팩터링 하기
new MemoryMemberRepository()가 중복되고 한눈에 역할과 구현을 파악하기 어렵다.
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new RateDiscountPolicy());
}
}
아래의 코드처럼 리팩터링을 하면 코드의 중복도 제거하고 전체적인 구성을 빠르게 파악할 수 있다.
→ 멤버 저장소는 어떤것인지, 할인 정책은 무엇을 쓰는지 빠르게 파악 가능
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}
이제 클라이언트 코드인 사용 영역을 변경하지 않고 AppConfig의 discountPolicy()만 변경하면 할인 정책을 변경할 수 있다.
테스트 코드 작성
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
//각 테스트 메서드가 실행되기 전 호출되는 메서드이다.
@BeforeEach
public void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
@Test
void createOrder(){
//given
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
//when
Order order = orderService.createOrder(member.getId(), "itemA", 10000);
//then
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
@BeforeEach로 테스트 메서드가 여러 개 있을 시 편리하게 사용할 수 있다.
스프링으로 전환하기
설정을 담당하는 AppConfig클래스에 @Conficuration을 적어준다.
메서드마다 @Bean을 붙여주면 반환되는 객체들이 스프링 컨테이너에 등록이 된다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@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 FixDiscountPolicy();
}
}
기존 코드에서 구현체를 가져오는 부분을 주석처리 하고 다음 2줄을 추가한다.
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
ApplicationContext를 스프링 컨테이너라고 한다.
스프링 컨테이너는 @Bean이 있는 모든 메서드를 호출하여 반환된 객체를 등록한다. 이 객체를 스프링 빈이라고 한다.
스프링은 기본적으로 객체를 생성하고, 메서드 이름으로 등록한다. → (key:메서드이름, value:객체) @Bean(name=”${NAME}”)이라고 적으면 별도로 설정이 가능하다.
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
스프링 컨테이너에서 스프링 빈을 가져오기 위해 다음과 같이 적어준다.
getBean(스프링 빈 이름, 반환 타입)이다.
위의 2줄을 추가한 전체 코드이다.
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 emember = " + findMember.getName());
}
}
그 후 실행을 해보면 결과는 같지만 로그들이 추가되어 출력된다.
12:58:25.434 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@33e5ccce
12:58:25.440 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
12:58:25.493 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
12:58:25.494 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
12:58:25.494 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
12:58:25.495 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
12:58:25.498 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
12:58:25.500 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberService'
12:58:25.508 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberRepository'
12:58:25.509 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
12:58:25.510 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'discountPolicy'
new member = memberA
find emember = memberA
스프링 빈들이 등록이 되는 로그인데 Bean이 생성되는 것을 확인할 수 있다.
첫 5개는 스프링이 내부적으로 필요해서 등록하는 Bean이고 그 다음부터는 직접 등록해준 Bean들이 등록되는 것을 볼 수 있다.
출처
'Spring 공부' 카테고리의 다른 글
스프링 빈 조회 (0) | 2022.07.11 |
---|---|
스프링 컨테이너 생성 과정 (0) | 2022.07.07 |
IoC, DI, 컨테이너 (0) | 2022.07.06 |
SOLID (0) | 2022.07.05 |
스프링과 객체지향 (0) | 2022.07.05 |