Spring

728x90

✔의존관계 주입 방법

의존관계 주입은 크게 4가지 방법이 있다.

  1. 생성자 주입
   public class MyService {    
   	private final MyRepository myRepository;     
   	
       @AutoWired
   	public MyService(MyRepository myRepository) {        
   		this.myRepository = myRepository;    
   	}     
   	// ... rest of the class implementation 
   }

위 예시처럼 생성자에 @AutoWired 설정을 통해 의존관계를 주입한다.

생성자를 통해 의존관계를 주입할때 생성자 호출시점에 1번만 호출되는것을 보장하도록 만들며 이는 불편,필수 의존관계에 사용한다.(주로)

❗❗이때, 생성자가 1개있다면 @AutoWired 생략이 가능하다.

 

📌 4가지의 의존관계 주입중 생성자 주입을 사용하는것이 권장된다.

  • 대부분의 의존관계 주입은 한번 발생하면 에플리케이션 종료 시점까지 의존관계를 번경할 일이없기때문에 (불변) 생성자 주입 사용을 권장한다.
  • 수정자를 통해 의존관계를 주입한다면 test코드를 짤때 수정자를 통해 어떠한 값을 설정해야하는지 파악이 어렵기때문에(클래스에 들어가봐야 알수있다) 누락될수있다. 하지만 생성자 주입이라면 테스트 코드를짤때 객체생성시 생성자에 어떠한값을 넣을지 파악할수있기때문에 누락될일이 없다.
  • 생성자 주입을 사용한다면 Final 을 사용할수있다. 이렇게되면 우리가 최기화 해야하는 필드를 초기화하지 않는 실수를 막을수있다. (생성자 주입만이 final 키워드 사용가능, 나머지 주입 방식은 모두 생성자 이후에 호출되기때문에 final 선언 불가!)
  •  

 

  1. 수정자 주입(setter 주입)
public class MyService {    
    private MyRepository myRepository;     
    
    @AutoWired
    public void setMyRepository(MyRepository myRepository) {        
    	this.myRepository = myRepository;    
    }     
    	// ... rest of the class implementation 
}

위 예시처럼 클래스에 존재하는 setter를통해 의존관계를 주입할수 있다. 이때는 @AutoWired 를 생략할수 없으며 이러한 방식은 선택,변경 가능성이 있는 의존관계에 사용한다.

@Autowired는 주입할 대상이없을때(여기선 myRepository) 오류를 발생시키는데, 주입할 대상이 없더라도(myRepository) 동작하게 하려면 @Autowired(required = false) 로 지정하면 된다.

 

📌생성자 를 통한 의존관계 주입은 bean을 등록할때 같이 일어나지만 수정자 즉 setter로 의존관계를 주입할때는 bean등록 이후에 의존관계 설정이 된다.

 

 

  1. 필드 주입
public class MyService {
    @Autowired
    private MyRepository myRepository;

    // ... rest of the class implementation
}

위 예시처럼 사용하면 필드를 통해 의존관계를 주입할수 있다. 하지만 필드를 통해 의존관계를 주입하는것은 권장되지않는다.

setter가 존재하지않는이상 외부에서 myRepository에 접근할수 있는 방법이 없기때문에 테스트하기 힘들다는 단점때문에 사용하지 않는다

 

  1. 일반 메서드 주입
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;

@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
	this.memberRepository = memberRepository;
	this.discountPolicy = discountPolicy;
}

위 예시처럼 일반 메서드에 @Autowired 를 붙여서 의존관계 주입을 해줄수도있다. 이렇게하면 한번에 여러필드를 주입받을수있지만 앞서말한 생성자주입이나 수정자주입을 통해 대체할수 있으므로 잘 사용하지 않는다.

 

 

 

✔옵션처리

주입할 스프링빈이 없더라도 @Autowired 가 동작하도록 만들수 있게 하려면 자동주입 대상으로 옵션처리를 하면된다.

 

  1. @Autowired(required=false)
    • 자동 주입할 대상이 없다면 수정자 메서드 호출 자체가 안된다.
  2. org.springframework.lang.@Nullable
    • 자동 주입할 대상이 없으면 null이 입력된다.
    • 📌@Nullable optional은 스프링 전반에 걸쳐 사용할수가 있다.
  3. Optional<>
    • 자동 주입할 대상이 없으면 Optional.empty가 입력된다

 

 

 

✔ lombok

롬복 라이브러리가 제공하는 @RequiredArgsConstructor 기능을 사용한다면 final로 선언한 필드에 대한 생성자를 자동적으로 만들어준다.

@RequiredArgsConstructor
public class Person {
    private final String name;
    private final int age;
    private String address;
    
    // constructor
    public static void main(String[] args) {
        Person person = new Person("kim",20);
    }
}

이런식으로 생성자를 따로 만들지 않더라도 @RequiredArgsConstructor 를 통해 final 필드에 대한 생성자가 존재하는것을 볼수있다.

 

 

✔조회하는 bean이 2개 이상일때

@Autowired 를 사용하면 Type을 통해 bean을 조회하는 방식을 사용한다. 이때 타입이 같은 2개의 빈이 존재한다면 (이름은 다르지만) 오류가 발생하는데 이를 해결하는 방법은 다음과 같다.

 

  1. @Autowired 필드 명 매칭
   @Autowired
   private DiscountPolicy discountPolicy 

FixDiscountPolicy, RateDiscountPolicy 클래스는 DiscountPolicy 인터페이스를 implement받는다. 즉 위와같이 적으면 type이 같은 bean이 2개이므로 오류가난다.

   @Autowired
   private DiscountPolicy rateDiscountPolicy

이때 위와같이 필드명을통해 우리가 사용할 bean의 이름을 명시해주면 오류를 제거해줄수 있다.

 

 

  1. @Qualifier
  2. @Qualifier 를 통해 추가 구분자를 붙여줄수 있다.
   @Component
   @Qualifier("mainDiscountPolicy")
   public class RateDiscountPolicy implements DiscountPolicy {}
   
   @Component
   @Qualifier("fixDiscountPolicy")
   public class FixDiscountPolicy implements DiscountPolicy {}

위와같이 추가구분자를 붙여준후

   public OrderServiceImpl(MemberRepository memberRepository,
   				@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy){
   		...
   }

생성자에 @Qualifier 를 사용하여 우리가 어떠한 bean을 사용할건지 명시해줄수 있다.

이때 @Qualifier 는 생성자 주입 뿐만아니라 수정자 주입 등에도 사용할수 있다.

 

 

  1. @Primary
  2. @Primary를 통해 위와같은 상황에서 RateDiscountPolicy, FixDiscountPolicy 2개가 존재할때
   @Component
   @Primary
   public class RateDiscountPolicy implements DiscountPolicy {}

@Primary 를 지정해주면 우선적으로 RateDiscountPolicy을 사용할수 있다.

 

❗❗ @Primary@Qualifier가 존재할때 우선권은 @Qualifier 이 더 높다.

Spring에서는 자동보다는 수동이, 넒은 범위의 선택권 보다는 좁은 범위의 선택권이 우선순위가 높다. 따라서 여기서도 @Qualifier(세세한 설정이 가능한)의 우선순위가 더 높은것이다.

 

 

 

✔ Annotation 만들기

Annotation을 만들면 컴파일 단계에서 잡을수없는 @Qualifier("mainDiscountPolicy") 을 (문자를 넣을때 실수할수 있기때문에 ex) mmainDiscountPolicy) Annotation을 만들면 추후 문자열을 잘못 입력했을때 컴파일단계에서 오류를 잡을수 있다.

 

 

📌 조회한 빈이 모두 필요할때 List, Map

위의 예시에서 FixDiscountPolicy, RateDiscountPolicy 클래스는 DiscountPolicy 인터페이스를 implement받는다. 이때 DiscountPolicy 타입의 bean을 모두 조회하고싶다면 List,Map을 사용하면 된다.

🙏Reference

728x90

'Spring' 카테고리의 다른 글

[SPRING] Bean Scope  (0) 2023.03.05
[SPRING] Bean Life cycle, call back  (0) 2023.03.05
[SPRING] Component scan  (0) 2023.03.03
[SPRING] Singleton  (0) 2023.02.26
[SPRING] Spring Container, Bean  (0) 2023.02.05

[SPRING] Component scan

2023. 3. 3. 03:41
728x90

기존엔 Appconfig.class에서 Spring bean으로 사용하려면 @Bean을 붙여줬지만 Component Scan을 사용하면 @Bean 없이 자동적으로 bean이 추가되도록 할수있다.

@Configuration
@ComponentScan(
        basePackages = "hello.core.member"
)
public class AutoAppConfig {

}
@Component
public class MemberServiceImpl implements MemberService{
    ...

@Component Scan을 설정정보에 붙이면 ComponentScan을 사용할수 있게 된다.

이렇게 되면 @Component설정정보를 가진 모든 클래스들이 (ex)MemberServiceImpl...) bean으로 등록된다.

❗❗이때 @Configuration설정정보가 들어있는것들은 모두 Component Scan의 scan대상이된다. 이 이유는 @Configuration소스코드를 열어보면 @Component 어노테이션이 붙어있기 때문이다.

@Component
public class OrderServiceImpl implements OrderService {
    ...
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
    discountPolicy) {
        ...
    }
}

이때 의존관계주입은 @Autowired를 사용하면된다

여기까지 정리를 해보자면 Component Scan에 의해 Component가 붙은 클래스를 bean으로 등록을 해준다 (bean의 이름은 맨앞글자만소문자+기존이름 ex)OrderServiceImpl -> orderServiceImpl) 의존관계 주입을 할때는 @AutoWired를 사용한다.

 

 

✔탐색 위치와 기본 스캔 대상

우리가 Component Scan을 사용한다 가정했을때 스캔할 범위를 지정해줘야한다. (매번 모든 클래스를 스캔하기엔 비효율 적이기 때문에)

@ComponentScan(
    basePackages = "hello.core",
}

basePackages : "hello.core" 를 포함한 하위 패키지까지

basePackageClasses: 클래스를 포함하는 패키지랑 하위 패키지 까지

default: @ComponentScan이 붙은 클래스의 패키지부터

따라서 default를 최상단 패키지에두면 편하며 이 방법 사용을 권장한다.

📌추가적으로 스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인 @SpringBootApplication 를 이 프로젝트 시작 루트 위치에 두는 것이 관례이다

 

 

✔ 필터

@Configuration 
@ComponentScan(    
    basePackages = "com.example.package",    
    includeFilters = {
    @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = MyComponent.class)
    @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*AnotherComponent$") 
    },    
    excludeFilters = {
    @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value =                                 ExcludeComponent.class),
    @ComponentScan.Filter(type = FilterType.CUSTOM, value = MyFilter.class)    
    } 
) 

public class AppConfig {    // ... }

includeFilters : 컴포넌트 스캔 대상을 추가로 지정할때 사용한다.
excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정할때 사용한다.

  • ANNOTATION: 지정된 어노테이션이 포함되는 클래스를 스캔합니다. (기본값 생략가능)
  • ASSIGNABLE_TYPE: 지정된 타입과 호환되는 클래스를 스캔합니다.
  • ASPECTJ: AspectJ 패턴을 사용하여 클래스를 스캔합니다.
  • REGEX: 정규식 패턴을 사용하여 클래스를 스캔합니다.
  • CUSTOM: 사용자 정의 필터를 지정합니다.

 

 

✔중복 등록과 충돌

경우의수1) 자동 빈 등록 vs 자동 빈 등록 (스프링에서 오류발생 ConflictingBeanDefinitionException)

경우의수2) 수동 빈 등록 vs 자동 빈 등록 (이 경우 수동 빈 등록이 우선권을 가진다.) 수동 빈이 자동 빈을 오버라이딩 해버린다

 

 

🙏Reference

728x90

'Spring' 카테고리의 다른 글

[SPRING] Bean Scope  (0) 2023.03.05
[SPRING] Bean Life cycle, call back  (0) 2023.03.05
[SPRING] 의존관계 자동 주입  (1) 2023.03.05
[SPRING] Singleton  (0) 2023.02.26
[SPRING] Spring Container, Bean  (0) 2023.02.05

[SPRING] Singleton

2023. 2. 26. 20:35
728x90

[SPRING] Singleton

✔Singleton pattern을 사용하는 이유

대부분 Spring 애플리케이션은 웹 애플리케이션이다.

웹 애플리케이션은 아래와같이 여러 클라이언트의 요청이 동시에 들어오는 경우가 많다.

1

이때 여러 클라이언트로부터 동시에 요청이들어오면 각 클라이언트마다 필요한 객체를 생성해야하고, 따라서 메모리낭비가 발생하게된다.

이러한 현상 즉 각 클라이언트마다 새로운 객체가 생성됨에 따른 메모리낭비를 해결해줄수 있는것이 Singleton 패턴이다.

✔Singleton pattern이란

Singleton pattern은 앞서말했듯 각 클라이언트마다 객체를생성하는게 아닌 클래스 인스턴스를 하나만 생성하여 모든 클라이언트에 대한 작업을 처리한다.

❗ Singleton을 생성하는 많은 방법이있지만 그중 객체를 미리 생성한뒤 static영역에 올려두는 방법으로 진행한다.

Singleton 패턴을 사용해보자.

private생성자를 사용하여 외부에서 임의로 new키워드를 사용하지 못하게 막는다.

private static final SingletonService instance = new SingletonService();

//객체 인스턴스를 조회할수있게 하는 메서드
public static SingletonService getInstance() {
    return instance;
}

또한 static영역에 미리 생성한 하나의 객체를 올려두는 방식을 사용한다.

이렇게 생성된 객체를 통해 클라이언트로부터 요청이 올때마다 새로운 객체를 생성하는것이아닌 기존에 생성해둔 객체를 사용하는것이다.

하지만 이렇게 생성된 클라이언트에는 다양한 문제점이 존재한다. 클라이언트가 구체 클래스에 의존하기 때문에 DIP를 위반하며, 따라서 OCP를 위반할 가능성이 높고, 생성자가 private이기 때문에 자식클래스를 만들기 힘들다. 이러한 문제점을 해결하기위해 나온것이 Singleton container이다.

✔Singeton Container

Singleton Container을 사용하면 위에서말한 Singleton의 단점을 상쇄시킬수있다.

Spring Container는 Singleton Container의 역활을하며 따라서 하나의 bean을 등록할때 Singleton방식을 사용한다.

ApplicationContext ac = newAnnotationConfigApplicationContext(AppConfig.class);

MemberService memberService1 = ac.getBean("memberService",MemberService.class);

MemberService memberService2 = ac.getBean("memberService",MemberService.class);

결론적으로 위 코드에서 memberService1과 memberService2가 같은 객체를 공유하는것을 확인할수 있다.

주의사항

Spring Container를 사용하면 위와같이 memberService1과 memberService2가 같은 객체를 공유한다.

이때 MemberService에 private int price; 와같은 상태를 유지하는 필드가 존재한다 가정하며, 이때 memberService1과 memberService2에대해서 각각 다른값을 지정해준다 생각해보자.

하지만 memberService1과 memberService2는 같은객체를 공유하기때문에 각각 다른값을 지정해줄수 없다. 따라서 Singleton pattern을 사용할때는 상태를 유지하지않는 무상태로 설계를 해야한다.

✔CGLIB

Spring container는 싱글톤 레지스트리다. -> Spring bean이 싱글톤이 되도록 보장을 해준다.

이때 bean이 싱글톤이 되도록 보장해주도록 하는것이 @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();
    }
    ...
}

AppConfig클래스에있는 메서드를 통해 bean으로 등록하려할때, @Configuration 을 사용해준다면, Spring은 CGLIB(바이트코드 조작 라이브러리)를 통해 각 bean의 싱글톤을 보장한다.

CGLIB를 사용해서 싱글톤을 보장하는 이유

AppConfig코드를 보면 memberService(),orderService() 각메서드가 memberRepository()를 호출하는것을 볼수있다. 즉 Singleton이 깨지는것처럼 (new MemoryMemberRepository(); 이 2번호출되니까) 보이지만 Spring은 이러한 문제를 CGLIB를 통해 해결한다.

2

CGLIB를 통해 위 그림과같이 AppConfig@CGLIB라는 클래스를 만들고 AppConfig가 아닌 AppConfig@CGLIB클래스를 사용해 각각의 bean들이 Singleton을 보장할수 있도록한다.

결과적으로 Spring은 각 빈의 싱글톤을 보장하기위해 CGLIB를 사용하며, CGLIB를 사용하기 위해서는 @Configuration을 사용해야 한다.

🙏Reference

728x90

'Spring' 카테고리의 다른 글

[SPRING] Bean Scope  (0) 2023.03.05
[SPRING] Bean Life cycle, call back  (0) 2023.03.05
[SPRING] 의존관계 자동 주입  (1) 2023.03.05
[SPRING] Component scan  (0) 2023.03.03
[SPRING] Spring Container, Bean  (0) 2023.02.05
728x90

Spring을 사용하는 이유

DIP 를 위반하는 간단한 예시

class LightBulb {
    void turnOn() {
        System.out.println("LightBulb: Bulb turned on...");
    }

    void turnOff() {
        System.out.println("LightBulb: Bulb turned off...");
    }
}

class Switch {
    private LightBulb lightBulb;

    public Switch() {
        this.lightBulb = new LightBulb();
    }

    void operate() {
        // ... some logic ...
        lightBulb.turnOn();
        // ... some logic ...
    }
}

위와 같은 예가 있다 가정한다. 위 예는 DIP(Dependency Inversion Principle) 를 위배한다. 그이유는 Switch 클래스가 LightBulb 라는 구체 클래스에 의존하기 때문이다. 이렇게되면 만약 lightBulb 가아닌 다른 빛을 내는 기기의 의존성을 주입받기 위해선 Switch 클래스를 수정해야한다.


이런 상황을 해결하기위해선

아래와같이 LightBulb 가 인터페이스를 상속받게 하며, Switch 클래스는 생성자를 통해 인터페이스 의존성을 주입받으면 된다.

interface SwitchableDevice {
    void turnOn();
    void turnOff();
}

class LightBulb implements SwitchableDevice {
    @Override
    public void turnOn() {
        System.out.println("LightBulb: Bulb turned on...");
    }

    @Override
    public void turnOff() {
        System.out.println("LightBulb: Bulb turned off...");
    }
}

class Switch {
    private SwitchableDevice device;

    public Switch(LightBulb lightBulb) {
        this.device = lightBulb;
    }

    void operate() {
        // ... some logic ...
        device.turnOn();
        // ... some logic ...
    }
}

위처럼 Switch 는 인터페이스인 SwitchableDevice 를 의존한다. 하지만 아직 문제가 있다. 아직도 LightBulb 외 다른 빛을 내는 기기의 의존성을 주입받기 위해서는 Switch 클래스의 생성자에 수정이 필요하다. 이럴땐 Switch 클래스의 생성자의 파라미터에 LightBulb 가 아닌 SwitchableDevice 를 넘겨주면 된다.


interface SwitchableDevice {
    void turnOn();
    void turnOff();
}

class LightBulb implements SwitchableDevice {
    @Override
    public void turnOn() {
        System.out.println("LightBulb: Bulb turned on...");
    }

    @Override
    public void turnOff() {
        System.out.println("LightBulb: Bulb turned off...");
    }
}

class Switch {
    private SwitchableDevice device;

    public Switch(SwitchableDevice device) {
        this.device = device;
    }

    void operate() {
        // ... some logic ...
        device.turnOn();
        // ... some logic ...
    }
}

class AppConfig {
    Switch configureSwitch() {
        return new Switch(new LightBulb());
    }
}

이렇게하면 이제 Switch 의 빛을 내는 기기를 바꾸고 싶을때 더이상 Switch 클래스를 건드리지 않아도 된다. 즉 AppConfig 라는 클래스에 우리가 사용하고 싶은 구체 클래스를 지정만 해주면 우리는 원하는 Switch 를 얻을수 있게 된다.



Spring 을 사용하는 이유는 다음과 같다.

  1. JAVA를 통해 코드를 짤때 객체를 구현하는 클래스를 Ex)AppConfig 처럼 다른 클래스로 분리해서 코드를 짜야한다. 그렇지 않으면 DIP(의존관계 역전 원칙)원칙 즉 하나의 클래스가 추상클래스와 구체클래스에 동시에 의존할수 있기때문이다.

    • 즉 DIP 의 핵심은 추상화에 의존을 해야한다는 것이다.
  2. 또한 AppConfig처럼 객체를 구현하는 클래스를 따로 분리하는순간 OCP원칙(소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다)에 의하여 우리가 소프트웨어 요소를 새롭게 확장해도 사용영역의 변경은 닫혀있으면서 AppConfig (구성영역)의 코드만 수정하면 되기때문에 OCP원칙도 지킬수 있게된다.

  3. 마지막으로 AppConfig를 통해 객체를 생성하고 연결하는 역활을 담당하게하고, 클라이언트 객체는 실행에 대한 책임만을 지게 함으로서 SRP(단일 책임 원칙)을 지킬수있다.

  4. 위 이유들 때문에 AppConfig처럼 구현객체를 생성하며 연결하는 클래스를 따로 구현한다. AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC컨테이너 또는 DI컨테이너라 한다.

  5. 기존에는 AppConfig를 사용해 직접 객체를 생성 및 의존관계를 주입했지만, 스프링 컨테이너를 사용해 위과정을 자동화 할수있다.!!!!



Container 생성, Bean등록

Spring Container에서는 @Configuration이 붙은 AppConfig를 설정(구성)정보로 사용한다. 이때 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.

 @Configuration
 public class AppConfig {

    @Bean
     public MemberService memberService() {
         return new MemberServiceImpl(memberRepository());
     }

   ...
 }

@Configuration에 의해 AppConfig을 구성정보로 사용한다는 의미이며, @Bean이라 적힌 memberService()에 의해 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 하며, 스프링 빈의 이름은 메서드의 명 (memberService)를 사용한다.



SpringContainer 에서 Bean 가져오는 방법

스프링을 사용함으로서 필요한 객체를 AppConfig에서 조회하는게 아닌 스프링 컨테이너에 등록된 스프링빈(객체)을 조회한다. 이때 스프링빈은 applicationContext.getBean()메서드를 통해 찾을수 있다.

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
 MemberService memberService = applicationContext.getBean("memberService", MemberService.class);

ApplicationContext을 통해 AppConfig안에 있는 환경설정 정보를 가지고 스프링 컨테이너를 만들어준다.

applicationContext.getBean("memberService", MemberService.class);을 통해 bean을 가져온다.

getBean("메서드이름",타입) 형식을 맞춰 입력해주면 된다.



Spring Container

Spring Container을 만드는 2가지 방법이존재한다.

  • XML을 기반으로 만들수있으며
  • 애노테이션 기반의 자바 설정 클래스로 만들수있다.(우리가 위에서 했던 방식)
  • 위 사진처럼 Spring Container에 Spring Bean이 어떻게 저장되는지 알수있다.
  • Bean의 이름은 메서드이 이름, 빈 객체는 메서드의 return 객체가 되는것을 알수있다.

🙏Reference

  • [김영한님 Spring 강의
728x90

'Spring' 카테고리의 다른 글

[SPRING] Bean Scope  (0) 2023.03.05
[SPRING] Bean Life cycle, call back  (0) 2023.03.05
[SPRING] 의존관계 자동 주입  (1) 2023.03.05
[SPRING] Component scan  (0) 2023.03.03
[SPRING] Singleton  (0) 2023.02.26

+ Recent posts