김영한님의 스프링 핵심 원리 - 기본편을 바탕으로 작성하였습니다.
[Spring] 싱글톤 컨테이너 와 이어지는 포스팅입니다.
전체 소스코드 보기
다양한 의존관계 주입방법
의존 관계 주입의 방법은 크게 4가지가 있다.
- 생성자 주입
- 수정자 주입(setter)
- 필드 주입
- 일반 메서드 주입
생성자 주입
말그대로 생성자를 통해서 의존관계를 주입하는 방법이다.
이전의 포스팅에서 사용한 방법이 전부 생성자 주입이다.
특징으로는 생성자 호출 시점에 딱 한번만 호출되는것이 보장되고,
불변적이고 필수적인 의존관계에서 사용 된다는 점이다.
예제로 이전에 작성했던 OrderServiceImpl
을 확인해보자.
@Component
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
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);
}
public MemberRepository getMemberRepository() {
return memberRepository;
}}
위의 코드를 확인 해보면,memberRepository
와 discountPolicy
가 생성자 호출 시 정해지고,
외부에서 변경이 불가능하다.
즉, setter가 없다.
생성자가 하나인 경우는
@Autowired
를 생략해도 의존성이 주입된다.
물론, 스프링 빈에만 해당하는 내용이다.
생성자 주입 방식은 일반적인 빈라이프 사이클과 달리,
빈의 생성과 의존관계 주입이 함께 일어난다.
빈 라이프사이클
- 컨테이너 생성
- 빈 생성
- 의존 관계 주입
수정자 주입
setter(수정자)를 통해 의존관계를 주입하는 방법을 말한다.
위의 코드를 수정자 주입방식으로 변경하면 다음과 같다.
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@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);
}
public MemberRepository getMemberRepository() {
return memberRepository;
}}
위와 같이 각각의 수정자를 만들어 주고,@Autowired
어노테이션을 붙여주면 자동으로 의존관계가 주입된다.
특징으로는 선택적으로 의존관계를 주입할 수 있고,
변경이 가능하다는 점이다.
@Autowired
는 주입할 대상이 없으면 오류가 발생한다.
주입할 대상이 없어도 동자갛게 하려면@Autowired(required = false
로 지정하면 된다.
필드 주입
이름 그대로 필드에 바로 의존관계를 주입하는 방법이다.
@Component
public class OrderServiceImpl implements OrderService {
@Autowired private MemberRepository memberRepository;
@Autowired 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);
}
public MemberRepository getMemberRepository() {
return memberRepository;
}}
위와 같이 필드 옆에 @Autowired
를 붙여서 의존관계를 주입하는 방법이다.
보다시피 코드가 많이 간결해서 과거에는 많이 사용하였으나,
외부에서 변경이 아예 불가능하다.
스프링 컨테이너는 자동으로 의존관계를 주입해주지만,
순수 자바코드로 단위테스트를 진행하려고 할때는 테스트 진행 자체가 아예 불가능하다.
최근에는 사용하지 않는 추세이다.
실제 코드와 연관 없는 테스트 코드를 작성하거나 하는 경우에는 사용되는 경우가 있다.
일반 메서드 주입
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void init(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);
}
public MemberRepository getMemberRepository() {
return memberRepository;
}}
위의 코드와 같이 init
이라는 일반 메서드에 @Autowired
를 붙여 의존관계를 주입하는 방법이다.
한번에 여러 필드를 주입받을 수 있다은 특징이 있지만,
일반적으로 잘 사용되지 않는다. (굳이 쓸 필요가...)
옵션 처리
흔하지 않는 경우 이지만,
주입할 스프링 빈이 없어도 동작해야할 때가 있다.
자동 주입 대상은 옵션으로 처리하는 방법은 3가지가 있다.
@Autowired(require=false
: 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출이 안된다.org.springframword.lang.@Nullable
: 자동 주입할 대상이 없으면 null이 입력된다.Optional<>
: 자동 주입할 대사잉 없으면Optional.empty
가 입력된다.
테스트 코드를 통해 확인해보자.
public class AutowiredTest {
@Test
void autowiredOption() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
}
static class TestBean {
@Autowired(required = false)
public void setNoBean(Member member) {
System.out.println("member = " + member);
}
@Autowired
public void setNoBean2(@Nullable Member member) {
System.out.println("member = " + member);
}
@Autowired
public void setNoBean3(Optional<Member> member) {
System.out.println("member = " + member);
} }}
위의 테스트 실행결과
member = null
member = Optional.empty
위의 코드에서 Member
는 스프링 빈이 아니다.
setNoBean2
와 setNoBean3
는 각각 null과 Optional.empty 가 호출 되지만,@Autowired(require = false)
를 달아둔 setNoBean
같은 경우는 아예 호출이 되지 않았다.
생성자주입을 선택하라
과거에는 수정자주입과 필드주입을 많이 사용했지만,
최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다.
대부분의 의존관계 주입은 한번 일어나면 앵간해선 의존관계를 변경할 일이 없다.
변경할 일이 없는걸 넘어서 대부분은 변하면 안된다.(불변성)
그런데 만약 수정자 주입을 사용한다면,
setter를 public으로 접근제한자를 오픈해야 되는데,
이렇게 되면 누군가가 실수로 setter를 호출해 의존관계를 변경할 수도 있다.
이런 식으로 불변성을 지켜야 하는 필드에 대해
변경에 관한 메서드를 public으로 열어 두는 것은 결코 좋은 방식이 아니다.
반면, 생성자 주입방식은 객체를 생성할 때, 딱 1 번만 호출 되므로 이후에 호출되는 일이없다.
따라서, 불변하게 설계할 수 있다.
또한, 수정자 주입 같은 경우 누락의 경우도 생길 수 있는데,
이건 코드를 통해 확인해보자.
우선 OrderServiceImpl
을 setter를 통한 수정자 주입방식으로 변경해보자.
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
// @Autowired
// public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
// this.memberRepository = memberRepository;
// this.discountPolicy = discountPolicy;
// }
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
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);
}
public MemberRepository getMemberRepository() {
return memberRepository;
}}
생성자는 그냥 주석처리 해둠.
그리고 테스트 코드를 새로하나 작성해서 확인을 해보자.
public class OrderServiceImplSetterTest {
@Test
void createOrder() {
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}}
위의 테스트 코드는 순수 자바로 단위테스트를 진행하는 과정이라고 생각하면 편할듯.
당연하게도 테스트 코드가 정상적으로 돌아가질 않는다.
NullPointException
이 뜨는데, setter를 호출하지 않았기 때문에 null 이 뜬다.
반면 다시 생성자주입 방식으로 돌려주면,
생성자에 파라미터 입력이 안되었다고, 컴파일 오류가 발생한다.
둘 다 실패하는 것은 똑같지만,
컴파일 과정에서 오류를 발견할 수 있고,
intelliJ 같은 IDE 에서도 빨간줄을 쫙쫙 그어주니,
생성자 주입방식이 실수의 확률이 적어진다.
그냥 생성자 주입을 하자..
롬복과 최신트렌드
막상 개발을 해보면 대부분이 불변이다.
근데 위에서 생성자 주입 사용하라고 그러는데,
생성자도 만들어야되고, 인자값 입력해주고,
필드에 할당도 해줘야하고 여간 귀찮은게 아니다.
이걸 좀 편하게 사용하는 방법을 알아보자.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
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);
}
public MemberRepository getMemberRepository() {
return memberRepository;
}}
위의 코드에 롬복이라는 라이브러리를 적용해서, 얘를 좀 간결하게 만들어보자.
우선 build.gradle에 롬복을 추가해주자.
//lombok configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
// lombok
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
//lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
//lombok
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
lombok 주석 사이에 있는 애들을 추가해주면 된다.
롬복 사용법은 나중에 따로 자세히 알아보고,
지금 롬복의 사용할 기능은 @RequiredArgsConstructor
이다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final 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);
}
public MemberRepository getMemberRepository() {
return memberRepository;
}}
이렇게 클래스레벨에 어노테이션을 붙여주면,
필드에서 final이 붙은 애들을 인자로 받는 생성자를 만들어준다.
순간, 어 @Autowired
없어서 의존관계 주입 안되는거 아닌가 싶엇는데,
생성자가 하나인 경우는 @Autowired
생략해도 가능하기 때문에, 정상적으로 잘 동작할거다.
코드도 간결하고 생성자주입방식을 사용할 수 있기에 매우 편-안 하다.
조회빈이 2개 이상
@Autowired
는 인자값의 타입으로 조회한다.
[Spring] - 스프링컨테이너 에서 확인 했듯,
같은 타입이 두개 이상일 때 ac.getBean
으로 타입만 이용해 조회하면,
NoUniqueBeanDefinitionException
이 발생한다.
마찬가지로 타입으로 조회하는 @Autowired
는 어떻게 되는지 확인해보자.
FixDiscountPolicy
와 RateDiscountPolicy
를 모두 스프링 빈에 등록하여 확인해보려 한다.
@Component
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
기존에 만들었던 FixDiscountPolicy
에 @Component
를 붙여서 스캔 대상으로 올려주었다.
그리고 이전 포스팅에서 만들었던 테스트를 다시 돌려보면,
마찬가지로 예외가 발생하는 것을 확인 할 수 있다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final 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);
}
public MemberRepository getMemberRepository() {
return memberRepository;
}}
위의 코드에서 컨테이너에 등록된 DiscountPolicy
타입의 빈을 조회 하는데,
같은 타입이 2개이상 발견되어서, 스프링이 어떤 빈을 가지고 의존관계를 주입해야 되는지 모르기 때문에
발생한 예외이다.
이런경우, DisocuntPolicy
타입이아닌 하위 타입(RateDisocuntPolicy
또는 FixDiscountPolicy
)을
지정하여 타입을 특정하면 예외를 해결할 수 있겠지만,
그렇게 되면 DIP원칙을 위배하게 되고, 유연성이 떨어진다.
해결방법을 하나씩 알아보자.
@Autowired 필드명 매칭
@Autowired
는 처음에는 타입매칭을 시도하고, 이때 같은타입의 빈이 여러개 있으면,
파라미터 이름으로 빈 이름을 추가 매칭한다.
이 특징을 이용하여 아래와 같이 변경해주면 된다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// 생성자 인자값의 이름을 변경해주었다.
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
@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);
}
public MemberRepository getMemberRepository() {
return memberRepository;
}}
롬복을 이용해서도 가능하다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
// 필드명 변경
private final DiscountPolicy rateDiscountPolicy;
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
// 여기서도 변경을 해줘야됨
int discountPrice = rateDiscountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
public MemberRepository getMemberRepository() {
return memberRepository;
}}
위와같이 필드명을 변경해주면 마찬가지로 잘 동작하지만,
필드명을 바꾸다보니 메서드에서 필드명을 참조하게 되는 모든 코드를 변경해줘야한다.
차라리 생성자를 만들어서 사용하는게 나을듯 하다.
@Qualifier
이번에는 @Qualifier
라는 어노테이션을 이용해 빈을 지정하는 방법을 알아보려한다.
참고로 @Qualifier
는 빈의 이름을 변경하는 것이 아니라,
지정을 위한 추가적인 이름을 지정해주는 방식이다.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
@Component
@Qualifier("FixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
}
이런 식으로 추가적인 이름을 지정할 수 있다.
그리고 의존관계를 주입받는 곳에서는 아래와 같이 사용하면 된다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") 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);
}
public MemberRepository getMemberRepository() {
return memberRepository;
}}
인자 옆에 @Qualifier("지정한 이름")
을 사용하여, 스프링 빈을 가져 올 수 있다.
롬복과 함께 사용할 때는 적용되지 않는다.
그리고 코드가 좀 더러워진다...
@Primary
@Primary
는 우선순위를 정하는 방법이다.@Autowired
에서 동일한 타입의 빈이 여러개 조회되는 경우,@Primary
가 붙은 빈이 우선적으로 적용된다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
이렇게 @Primary
만 붙여주면 된다.
기존의 OrderServiceImpl
을 전혀 건들 필요가 없이 사용할 수 있다.
협업단계에서 팀내의 룰만 잘 정해진다면 효율적으로 사용할 수 있다.
@Qualifier vs @Primary
만약 두개가 충돌 한다면 어떻게 될까.
예를 들어 RateDiscountPolicy에
@Primary
가 붙어있고,각각
@Qualifier
로 mainDiscountPolicy 와 fixDiscountPolicy 라는 보조네임을 지정해준 뒤OrderServiceImpl
에서@Qualifier
로 fixDiscountPolicy 를 지정해준다면?이 경우에는
@Qualifier
가 우선적으로 동작한다.
custom @Qualifier
@Qualifier
의 단점 중 한가지는 컴파일 단계에서 보조네임의 타입체크가 되지 않는 다는 점이다.(문자열이니까)
문자열 이기 때문에 오타가 날 수도 있기 때문에,
이런 경우는 어노테이션을 직접 만들어서 사용하면 좋다.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
이렇게 생성하면 된다.
그리고 RateDiscountPolicy
에다가 방금 만든 @MainDiscountPolicy
어노테이션을 붙여주면 된다!
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
} }}
그리고 OrderServiceImpl 은 아래와 같이 변경해준다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository,
@MainDiscountPolicy 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);
}
public MemberRepository getMemberRepository() {
return memberRepository;
}}
요렇게 해놓고 테스트를 돌려보면 기가막히게 동작하는걸 확인해볼 수 있다.
조회한 빈이 모두 필요할 때
의도적으로 정말 해당 타입의 스프링 빈이 다 필요한 경우도 있다.
이제껏 함께해온 예제코드로 예를 들어보자면,
클라이언트가 할인의 종류를 선택 할 수 있다고 가정해보자.
이러한 경우에는 RateDiscountPolicy
와 FixDiscountPolicy
두개가 모두 필요할 텐데,
이러한 경우는 어떻게 해야할까
디자인 패턴 중 전략패턴을 사용하면 되는데,
스프링에서는 이를 매우 쉽게 할 수 있다.
테스트 코드를 이용해 확인해보자.
public class AllBeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "kong", Grade.VIP);
int discountPrice = discountService.discount(member, 20000, "fixDiscountPolicy");
assertThat(discountPrice).isEqualTo(1000);
}
static class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policyList = " + policies);
}
public int discount(Member member, int price, String discountCode) {
return 0;
} }}
discount
의 로직은 아직 작성하지 않았지만,
이렇게 해놓고 실행을 해보면,
Map 과 List에 DiscountPolicy 타입의 모든 빈이 할당된 것을 출력을 통해 확인할 수 있다.
이제 로직을 작성하면 되는데 로직은 매우 간단하다.
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
}
이렇게 해주면 된다.
이렇게 해놓고 테스트롤 돌려보면 잘 통과가 된다.
이번엔 rateDiscountPolicy 를 입력하고 테스트를 돌려보자
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "kong", Grade.VIP);
int discountPrice = discountService.discount(member, 20000, "fixDiscountPolicy");
assertThat(discountPrice).isEqualTo(1000);
int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
assertThat(rateDiscountPrice).isEqualTo(2000);
}
따로 만들기 귀찮아서 그냥 두줄 추가했다.
discountCode 에 rateDiscountPolicy 로 입력을 했으니,
20000원의 10프로인 2000원으로 테스트를 돌려보니 마찬가지로 잘 통과하는 것을 확인할 수 있다.
음... 우테코 프리코스 덕에..
매직리터럴이 매우 불편하다...
Enum을 사용하면 깔끔하게 사용할 수 있을것 같다..!
'Study > Spring' 카테고리의 다른 글
[Spring] @Repository vs @Component 삽질로그 (0) | 2023.04.23 |
---|---|
[Spring] @ResponseBody 무쓸모 아님?(HttpMessageConverter, ResponseEntity) (1) | 2023.04.14 |
[Spring] 컴포넌트 스캔 (0) | 2023.02.01 |
[Spring] 싱글톤 컨테이너 (0) | 2023.01.31 |
[Spring] 스프링 컨테이너 (0) | 2023.01.19 |
댓글