싱글톤 패턴

 

클래스의 인스턴스가 1개만 생성되는 것을 보장하는 디자인 패턴이다.

 

그렇다면 객체 인스턴스를 2개 이상 생성하지 못하도록 하는 방법은?

private 생성자를 이용해 외부에서 new 키워드를 사용못하게 막기!

 

 

 

싱글톤 패턴이 생겨야 하는 이유

먼저 웹 애플리케이션을 살펴보자면 

보통 웹 애플리케이션은 여러 클라이언트의 요청이 동시에 들어오기 때문에 객체는 무한생성이 된다. 

아래 그림보면 고객이 3번 요청을 하면 3번 객체가 생성이 됨

 

 

위의 그림 코드

@Configuration
public class AppConfig {

	@Bean
	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}
	
	@Bean
	public MemoryMemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}
	
	@Bean
	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(), discountPolicy()); //discountPolicy()로 대체해주기
	}
	
	@Bean
	public DiscountPolicy discountPolicy() {
//		return new FixDiscountPolicy();  // 1만원이든 2만원이든 천원 할인적용
		return new RateDiscountPolicy(); // 1만원 천원할인, 2만원이면 2천원할인
	}
}

 

 

그렇다면 요청할때마다 객체가 계속 생성되는데 모두 다른지 확인해보기

 

[테스트]

public class SingletonTest {
 	@Test
 	@DisplayName("스프링 없는 순수한 DI 컨테이너")
 	void pureContainer() {
    
            AppConfig appConfig = new AppConfig();

            //1. 조회: 호출할 때 마다 객체를 생성
            MemberService memberService1 = appConfig.memberService();

            //2. 조회: 호출할 때 마다 객체를 생성
            MemberService memberService2 = appConfig.memberService();

            //참조값이 다른 것을 확인
            System.out.println("memberService1 = " + memberService1);
            System.out.println("memberService2 = " + memberService2);

            //memberService1 != memberService2
            assertThat(memberService1).isNotSameAs(memberService2);
     }
}

 

 

[결과] 

▶memberService1, memberService2 는 각각 새로운 MemberService 객체를 생성해낸다 

콘솔에 보이듯 요청할때마다 객체가 모두 새로 생성되어 다른 인스턴스인걸 확인할 수 있고

jvm 메모리에 계속 객체가 생성되고 소멸되어 메모리 낭비가 심해진다.

 

 

[해결방안]

그렇기 때문에 객체가 딱 하나만 생성을 해서 공유하도록 설계하는것이 '싱글톤 패턴'이라고 한다. 

 

 

 

 

 

싱글톤 패턴 적용하기

클래스의 인스턴스 1개만 생성하기

private 생성자를 사용하여 외부에 객체가 생성하지 못하도록 한다. 

 

 

싱글톤 패턴 적용한 테스트 코드를 main말고 test 폴더에 생성해보기 

[비 싱글톤]

public class NotSingletonService {

    //생성자
    private NotSingletonService(){
    }

    public NotSingletonService getInstance(){
        return new NotSingletonService();;
    }
}

getInstance() 를 호출할때마다 NotSingletonService 객체가 계속 생성되기 때문에 싱글톤 패턴이 아니다.  

 

 

 

[싱글톤]

public class SingletonService {

    //static 영역에 객체가 딱 1개만 생성되도록
    private static final SingletonService instance = new SingletonService();

    //생성자 private 을 통해 외부로부터 막음
    private SingletonService(){
    }

    //static 메서드를 통해 객체 인스턴스를 조회하도록 
    // instance 를 반환하여 동일한 객체가 반환됨.
    public static SingletonService getInstance(){
        return instance;
    }
}

 

1. private static final SingletonService instance = new SingletonService();

: static 영역의 instance 변수를 통해 SingletonService 객체를 생성한다.

 

2. private SingletonService() {} 

: 생성자 private을 통해 외부에서 객체를 생성하지 못하도록 막아준다.

 

3.  public static SingletonService getInstance(){
        return instance; }

:  getInstance() 메서드를 호출할때 이미 1번에서 객체가 생성된 상태여서 이미 생성된 동일한 객체가 반환이 된다. 

 

 

※ 여기서 SingletonService 클래스를 SingletonService  타입인 getInstance () 를 정의하는데
클래스 내에서 클래스를 다시 재정의 한다?


public static SingletonService getInstance(){
        return instance;
}

>> 자바는 클래스 내 클래스를 정의가능하며 이것을 중첩 클래스 또는 내부 클래스라고 한다.
그렇다면 내부 클래스를 정의하는 이유는? 
>> 외부적으로 사용될 일이 없고 내부적으로 별도의 클래스를 정의가 필요할때 사용한다. 
여기서는 SingletonService 타입을 반환하는 이유는 싱글톤 패턴에서 유일한 인스턴스( instance )를 가져오기 위한 설계 구조상 필요한 부분이기 때문에

 

 

 

 

instance 변수와 getInstance() 메서드에 static을 쓰는 이유??

  • 1. 클래스 레벨에서 접근 가능 / 간편 접근성
    • static으로 선언된 정적 변수인 instance는 클래스 로딩시점에 메모리에 올라가서 객체가 생성된다.
    • 그러므로 같은 클래스 내에 static으로 선언한 메서드에서 객체 생성 없이 바로 접근가능하다.
      • ▶ SingletonService.getInstance()
    • 2. 싱글톤 유지
      • 단 하나만의 객체를 생성하고 재사용하는데에 적합하다. 
    • static을 쓰면 클래스 로딩 시점에 객체를 생성하고 동시에 메모리에 할당하기 때문에 시점이 안 맞는 오류를 방지하기도 한다.
  • 만약 getInstance() 메서드를 static으로 선언하지 않으면?
    • 싱글톤으로 구현은 가능
    • getInstance() 메서드를 호출했을때 이미 객체가 생성된 이후여서 메서드를 호출할 수는 있다. 
    • 하지만 static을 선언하면 클래스 내의 명확한 접근성을 보장하기 때문에 쓰는게 좋다. 

 

 

 

[테스트]

public class SingletonTest {
    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    public void singletonServiceTest() {
        
        //private으로 생성자를 막아두었다. 컴파일 오류가 발생한다.
        //new SingletonService();
        //1. 조회: 호출할 때 마다 같은 객체를 반환
        SingletonService singletonService1 = SingletonService.getInstance();

        //2. 조회: 호출할 때 마다 같은 객체를 반환
        SingletonService singletonService2 = SingletonService.getInstance();

        //참조값이 같은 것을 확인
        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2)
}

 

[결과]

객체 같은걸 볼 수 있다. 

 

 

SingletonService는 private를 사용했기때문에 클래스 외부에서 객체 생성을 막아놨다. 

그래서 SingletonTest에서 new SingletonService()를 사용하지 않고, getInstance() 메서드를 통해 이미 만들어진 객체를 가져와서 인스턴스를 반환했다 

 

여기서 중요한 점은 getInstance() 메서드를 두번 호출했지만 반환되는 객체는 동일한 것!

이것이 싱글톤 패턴이다. 

 

 

 

 

싱글톤의 문제점

 

public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    private SingletonService(){
    }

    public static SingletonService getInstance(){
        return instance;
    }
}

 

  • 싱글톤 패턴을 구현하는 위의 코드를 모두 넣어야하는 문제점이 있다.  
  • private 생성자로서 자식클래스를 만들기 어렵다. 
  • 유연성이 떨어진다. (? 정확한 이해는 못하겠지만 DIP 위반이 되기 때문)

 

 

 

 

하지만 스프링 컨테이너는 싱글톤 패턴의 문제점을 해견해준다. 

 

반응형
LIST

관심사의 분리

인터페이스를 역이라 생각하고 

로미오의 역할(인터페이스)을 하는 디카프리오가 여주인공 역할인 줄리엣 역할(인터페이스)을 직접 캐스팅해야한다면 

디카프리오는 공연도 하고 동시에 여주인공도 캐스팅하는 '다양한 책임'을 가지고 있다 

 

 

 

[예시]

private DiscountPolicy discountPolicy = new FixDiscountPolicy(); 

 

OrderServiceIml 클래스에서 직접 객체(DiscountPolicy)를 생성하고 구체적으로 FixDiscountPolicy 를 선택하여 discountPolicy 에 할당한다. 

마치 디카프리오가 여주역할인 줄리엣 역할을 캐스팅하는 것 처럼

 

 

여기서 

관심사를 분리해보자.

 

배우는 본인의 역할만 수행하도록 집중해야한다  

공연을  구성하고 담당배우 캐스팅하는 것은 각 책임을 담당하는 공연 기획자를 만들어 확실히 분리를 하는 것!

인터페이스에 어떤 구현체가 할당될지는 공연 기획자가 해야한다 AppConfig가 공연기획자라고 생각하자

 

 

 

 

 

AppConfig 

 

AppConfig 등장

 

애플리케이션 전체 동작을 설정하고 구성(config)하기 위해 구현객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스이다. (배우들을 담당 역할을 실행하는 책임만 지면된다.)

public class AppConfig {
	public MemberService memberService() {
		return new MemberServiceImpl(new MemoryMemberRepository());
	}
}​

 

 

- 실제 동작에 필요한 구현객체를 생성한다 (memoryMemberRepository 객체 생성)

- 생성한 객체 인스턴스 참조를 생성자를 통해 주입해준다 (= Dependency injection 의존성 주입) 


MemoryMemberRepository 객체를 생성한 후
public class AppConfig {
	public MemberService memberService() {
		return new MemberServiceImpl(new MemoryMemberRepository());
	}
}​


MemberServiceImpl 클래스의 생성자를 생성 한 뒤 
MemoryMemberRepository 객체의 참조값을 전달한다.

public class MemberServiceImpl implements MemberService{
    public MemberServiceImpl(MemberRepository memberRepository) {
         this.memberRepository = memberRepository;
    }
}​

 

 

의존성 주입

  • MemberServiceImpl 은 MemberRepository 인터페이스에만 의존하고
  • MemberRepository 인터페이스를 구현한 MemoryMemberRepository 구현객체에는 의존하지 않음
  • 예를들어 MemberRepository 인터페이스를 구현만 한다면 MemoryMemberRepository 에서DatabaseMemberRepository 로 바꾸어도  MemberServiceImpl 은 변경 없이 DatabaseMemberRepository 주입받아 사용가능하다. 
  • MemberServiceImpl 입장에는 의존관계를 외부(AppConfig)에서 주입해주는 것 같다해서 DI '의존성 주입'이라고 한다.
  • MemberServiceImpl 은 실행에만 집중하면 된다.

 

 

 


 

 

과정 정리 

 

 

1. 실행할때 Appconfig 를 통해 memberService 를 달라고 하면 

public class MemberApp {

	public static void main(String[] args) {
		//관심사 분리 DI
		AppConfig appConfig = new AppConfig();
		MemberService memberService = appConfig.memberService(); 
        
        ...
      }
}

 

 

2. MemberServiceImpl 객체를 반환하면서 생성자로 MemoryMemberRepository 를 참조값으로 주입하여 사용한다. 

public class AppConfig {
	public MemberService memberService() {
		return new MemberServiceImpl(new MemoryMemberRepository());
	}
}​

 

 

 


 

여기까지 AppConfig 에서는 역할에 어떤 구현을 하는지 한눈에 보여야하는데

MemoryMemberRepository() 역할이 안보여서 리팩토링을 해보자

 

AppConfig  리팩터링 

 

 

 

리팩터링이란?

 

코드내부의 기능은 바꾸지 않고 코드 중복 제거하여 가독성을 높이고 개선하는 방식으로 재조정하는 것

 

 

 

 

어떻게? 

new MemoryMemberRepository() 스크랩하고 alt + shift + m 단축키 누르고 Extract Method 창을 연다 

 

 

* Ctrl + Alt + M(Extract Method) 단축키가 보편적인데 안되길래

검색하면 바로 나오는 해결방법이 Geforce Experience 앱에 단축키를 먹어서라고.. 근데 삭제를 해도 안됨

알고보니 단축키가 다른거였다

 

 

 

memberRepository 라고 변경하면 MemoryMemberRepository 에서 memberRepository 로 모두 바뀐다

 

 

[리팩터링 전]

public class AppConfig {
	public MemberService memberService() {
		return new MemberServiceImpl(new MemoryMemberRepository());
	}

	public OrderService orderService() {
		return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
	}
}

 

 

[리팩터링 후]

public class AppConfig {

	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}
	
    //리팩터링
	public MemoryMemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}
	
	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(),new FixDiscountPolicy()); 
	}
}

 

MemoryMemberRepository 를 반환해주는 역할을 만들어서

memberService, orderService 의 참조값에 반복되는 new MemoryMemberRepository() 객체를 대신해 사용한다.

 

예를들어 DatabaseMemberRepository 로 변경하면 일일이 바꿔줄 필요 없이 해당 코드만 바꿔주면 다 변경가능하니까 

코드가 쉬워진다.

public MemoryMemberRepository memberRepository() {
    return new DatabaseMemberRepository();
}

 

 

 

orderService() 부분도 다시 리팩터링 해보자면 

discountPolicy()을 만들어줌으로써 리턴값만 바꿔주면

FixDiscountPolicy() , RateDiscountPolicy() 을 사용하는 리턴값들을 일일이 바꿀 필요가 없다.

public OrderService orderService() {
    return new OrderServiceImpl(memberRepository(), discountPolicy()); //discountPolicy()로 대체해주기
}

public DiscountPolicy discountPolicy() {
	return new FixDiscountPolicy(); 
	//return new RateDiscountPolicy(); 
}

 

 

 

리팩터링 장점

  • 역할을 구현 명확히 분리
  • 특히 역할이 잘 드러남
  • 중복 제거

 

반응형
LIST

 

빈 생명주기 콜백이란?

 

 

'빈 생명주기 콜백'은 스프링 빈이 생성되고 소멸될 때 즉 초기화 작업과 종료 작업을 수행할수 있도록 하는 것이다.

자세하게는 스프링에서 빈의 생성, 초기화, 소멸과 관련된 작업을 수행할 수 있도록 하는 기능이다.

 

 

초기화 작업과 종료 작업이 무슨 작업이지?? 

 

1. 애플리케이션 서버가 시작될때 DB 와 연결설정하는 초기화 작업

보통 애플리케이션은 관계형 데이터베이스를 연결하는데 오래걸리기 때문에

애플리케이션 실행시(서버가 시작될 때) 미리 DB와 애플리케이션을 연결시켜놓는다 

그렇게 하면 여러 요청이 들어올때 새롭게 DB와 연결할 필요가 없어진다.

 

2. 요청이 올때마다 미리 생성된 DB 연결을 재활용

다른 고객의 요청이 올때 DB와 연결을 재사용함으로써 성능이 향상되는 장점이 있다.

 

3. 서버 종료 시 DB연결 미리 끊는 종료 작업

또한 종료될때도 서버가 확 꺼지는게 아니라 애플리케이션 컨텍스트(ApplicationContext) 가 종료될때 DB와의 연결도 함께 미리 종료하여 서버를 종료한다. 그렇게 되면 정상적이고 안전하게 종료 처리가 된다. 

 

 

 

이러한 초기화 작업과 종료 작업은 어떻게 진행되는건지 예제코드로 알아보자 

예제코드니 가짜 네트워크를 연결해보고 단순히 문자로만 출력되도록 해보기 

 

 

[예제코드]

서비스 시작할때 호출되는 connect() 메서드와 메세지를 호출하는 call() 메서드

그리고 서비스가 종료될때 disconnect() 메서드를 호출하여 연결을 끊어야한다. 

public class NetworkClient {
    private String url;
	
    //생성자
    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메시지");
    }
    
    public void setUrl(String url) {
    	this.url = url;
    }
    
	//서비스 시작시 호출하는 메서드 : connect() 와 call() 
    public void connect() {
    	System.out.println("connect: " + url);
    }
    
	//메세지:연결한 서버에 메세지를 던져주는 메서드
    public void call(String message) {
    	System.out.println("call: " + url + " message = " + message);
    }
    
	//서비스 종료시 호출되는 메서드 : disConnect()
    public void disconnect() {
    	System.out.println("close: " + url);
    }
}

 

public class BeanLifeCycleTest {

	@Test
	public void lifeCycleTest() {
		//빈 생명주기 
		//1. 스프링 컨테이너 생성
		ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
		
		//컨테이너로부터 빈 조회 
		//(이미 LifeCycleConfig을 스프링 컨테이너에 넣었고 networkClient()에서 NetworkClient 빈을 등록했기떄문에 조회만 함)
		NetworkClient client = ac.getBean(NetworkClient.class); 
		
		ac.close(); // ApplicationContext > ConfigurableApplicationContext > AnnotationConfigApplicationContext
	}
	
	@Configuration
	static class LifeCycleConfig {
		
		//2. 빈 생성
		@Bean
		public NetworkClient networkClient() {
			NetworkClient networkClient = new NetworkClient();
			networkClient.setUrl("http://hello-spring.dev");
			return networkClient; //리턴시 메서드 호출되는 시점에 객체생성이 되고 빈이 등록된다.
		}
	}
}

 

 

[로그 출력]

생성자 호출, url = null
connect: null
call: null, message = 초기화 연결 메세지

 

생성자 부분을 보면 url 값은 null이 출력된다. 

왜일까 networkClient.setUrl("http://hello-spring.dev"); 을 통해 값을 넣었는데 왜 null이 나올까??

 

스프링 빈의 생명주기를 보면

객체 생성 이후 의존관계 주입이 이루어진다.

객체 생성은 되었으나 의존관계 주입이 이루어지지 않았기때문에 url 값이 null로 출력이 되는 것!

스프링 빈은 객체를 생성하고 의존관계 주입이 모두 끝난 이후에 데이터를 사용할 준비가 된다. 

 

 

 

  • 스프링 빈의 생명주기
  1. 스프링 컨테이너 생성
  2. 스프링 빈 생성
  3. 의존관계 주입
  4. 초기화 콜백 사용 : 초기화 작업
    • 객체 생성 및 의존관계 주입 이후 객체를 사용하기 위해 필요한 추가 작업 정도로만 알기
    • 의존관계 주입된 후 초기화 콜백 메서드가 호출되므로 개발자에게 초기화 시점을 알려주는 목적
  5. 소멸 전 콜백 : 종료작업
    • 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 진행할 수 있다.
    • 소멸 콜백 메서드 호출하여 개발자에게 알려줄 수 있다. 
  6. 스프링 종료 : 애플리케이션 종료될때 스프링 컨테이너 종료, 남아있는 빈들의 소멸작업 수행

 

이렇게 초기화 콜백은 빈이 생성되고 의존관계 주입이 완료 후 호출이 되고 

소멸전 콜백은 빈이 소멸되기 직전에 호출되는것

 

 

참고로 객체 생성과 초기화를 분리하는게 일반적으로 객체 지향 프로그램의 원칙 중 하나인 단일책임 원칙을 따른다 

이렇게 명확히 나누는것이 유지보수 관점에서 좋은 설계이다. 

  • 객체 생성은 생성자에서 필수 정보를 파라미터로 받고 메모리에 할당하여 객체를 생성하는 책임을 가진다
  • 초기화는 생성된 값을 동작하도록 별도의 초기화 메소드로 분리하여 외부 커넥션을 연결하는 작업을 가진다  

 

 

 

  • 스프링은 3가지 방법으로 빈 생명주기 콜백을 지원한다.
  1. 인터페이스(InitializingBean, DisposableBean)
  2. 설정 정보에 초기화 메서드, 종료 메서드 지정
  3. @PostConstruct, @PreDestroy 어노테이션

 

 

 

빈 등록 시 초기화 메서드와 소멸 메서드

InitializingBean, DisposableBean 인터페이스

 

2003년에 나온 스프링 초창기 방법이라 거의 사용하지는 않음.

 

  • InitializingBean 는 afterPropertiesSet() 메서드로 초기화를 한다.
  • DisposableBean 는 destroy() 메서드로 소멸을 한다. 
public class NetworkClient implements InitializingBean, DisposableBean{ //초기화 빈, 종료 빈

	private String url;

	//생성자
	public NetworkClient() {
		System.out.println("생성자 호출, url = " + url);
	}

	//외부에서 setter로 넣을 수 있도록
	public void setUrl(String url) {
		this.url = url;
	}
	
	//서비스 시작시 호출하는 메서드
	public void connect() {
		System.out.println("connect: " + url);
	}

	//연결된 상태에서 부를 수 있는 메서드 (연결한 서버에 메세지를 던져준다)
	public void call(String message) {
		System.out.println("call: " + url + ", message = " + message);
	}
	
	//서비스 종료시 호출되는 메서드 
	public void disConnect() {
		System.out.println("close: " + url);
	}

	//InitializingBean 의존관계 주입이 끝나면 호출하는 '초기화 메서드'
	@Override
	public void afterPropertiesSet() throws Exception {
		System.out.println("NetworkClient.afterPropertiesSet");
		connect();
		call("초기화 연결 메세지");
	}

	//DisposableBean 종료시 호출하는 '소멸 메서드'
	@Override
	public void destroy() throws Exception {
		System.out.println("NetworkClient.destroy");
		disConnect();
	}
}

 

 

[테스트 클래스는 그대로]

public class BeanLifeCycleTest {

	@Test
	public void lifeCycleTest() {
		ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
		NetworkClient client = ac.getBean(NetworkClient.class); //빈조회
		ac.close(); // ApplicationContext > ConfigurableApplicationContext > AnnotationConfigApplicationContext
	}
	
	@Configuration
	static class LifeCycleConfig {
		
		@Bean
		public NetworkClient networkClient() {
			NetworkClient networkClient = new NetworkClient();
			networkClient.setUrl("http://hello-spring.dev");
			return networkClient;
		}
	}
}

 

 

[결과로그]

 

 

 

 

 

 

빈 등록 초기화, 소멸 메서드 지정하기

@Bean(initMethod = "init", destroyMethod = "close")

 

설정 정보 사용 특징

  • 초기화, 소멸전 콜백 메서드 이름을 자유롭게 정할 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않는 장점이 있다. (NetworkClient 클래스 코드를 보면 알 수 있음)
  • 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용가능하다.
public class NetworkClient{ 

	private String url;

	//생성자
	public NetworkClient() {
		System.out.println("생성자 호출, url = " + url);
	}

	//외부에서 setter로 넣을 수 있도록
	public void setUrl(String url) {
		this.url = url;
	}
	
	//서비스 시작시 호출하는 메서드
	public void connect() {
		System.out.println("connect: " + url);
	}

	//연결된 상태에서 부를 수 있는 메서드 (연결한 서버에 메세지를 던져준다)
	public void call(String message) {
		System.out.println("call: " + url + ", message = " + message);
	}
	
	//서비스 종료시 호출되는 메서드 
	public void disConnect() {
		System.out.println("close: " + url);
	}

	//초기화 콜백메서드
	public void init() {
		System.out.println("NetworkClient.init");
		connect();
		call("초기화 연결 메세지");
	}

	//소멸전 콜백메서드
	public void close() {
		System.out.println("NetworkClient.close");
		disConnect();
	}
}

 

 

[테스트 클래스 BeanLifeCycleTest]

public class BeanLifeCycleTest {

	@Test
	public void lifeCycleTest() {
		ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
		NetworkClient client = ac.getBean(NetworkClient.class); //빈조회
		ac.close(); // ApplicationContext > ConfigurableApplicationContext > AnnotationConfigApplicationContext
	}
	
	@Configuration
	static class LifeCycleConfig {
		
		@Bean(initMethod = "init", destroyMethod = "close")
		public NetworkClient networkClient() {
			NetworkClient networkClient = new NetworkClient();
			networkClient.setUrl("http://hello-spring.dev");
			return networkClient;
		}
	}
}

 

 

 

[결과로그]

 

 

 

 

마지막으로 어노테이션 사용하는걸 권장

@PostConstruct, @PreDestroy 사용하기

 

  • 최신 스프링에서 가장 권장하는 방법
  • 어노테이션만 추가하면 되므로 가장 편리
  • 컴포넌트 스캔과 어울린다. 
  • 유일한 단점은 외부 라이브러리와 적용을 못한다. 코드를 고쳐야하기 때문!
    • 만약 외부 라이브러리를 초기화, 종료해야한다면 두번째 방법대로 @Bean 의 initMethod , destroyMethod 사용하기

 

위의 NetworkClient 클래스 코드에서 초기화, 종료 메서드에 어노테이션만 추가하기 

@PostConstruct
public void init() {
    System.out.println("NetworkClient.init");
    connect();
    call("초기화 연결 메세지");
}

@PreDestroy
public void close() {
    System.out.println("NetworkClient.close");
    disConnect();
}

 

 

[테스트 클래스 BeanLifeCycleTest]

public class BeanLifeCycleTest {

	@Test
	public void lifeCycleTest() {
		ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
		NetworkClient client = ac.getBean(NetworkClient.class); //빈조회
		ac.close(); // ApplicationContext > ConfigurableApplicationContext > AnnotationConfigApplicationContext
	}
	
	@Configuration
	static class LifeCycleConfig {
		
		@Bean
		public NetworkClient networkClient() {
			NetworkClient networkClient = new NetworkClient();
			networkClient.setUrl("http://hello-spring.dev");
			return networkClient;
		}
	}
}

 

 

 

[결과로그]

 

 

 

 

반응형
LIST

 

 

기존에 공부했던 AutoAppConfig 는 @ComponentScan 어노테이션이 붙어있어서  @Component 가 붙은 클래스를 모두 스캔한다.  

@Configuration 
@ComponentScan(
		basePackages = "hello.core.member", //member 패키지를 포함한 하위 패키지까지 컴포넌트 스캔 대상이 됨
		basePackageClasses = AutoAppConfig.class, //지정한 클래스의 패키지만 컴포넌트 스캔 대상이 됨 
		excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

}

 

 

@Component가 붙은 FixDiscountPolicy,RateDiscountPolicy 

@Component
public class FixDiscountPolicy implements DiscountPolicy{
	~~
}​

@Component
public class RateDiscountPolicy implements DiscountPolicy{
	~~
}​


 

 

 

[AllBeanTest]

public class AllBeanTest {

	@Test
	void findAllBean() {
		ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
	}
	
	static class DiscountService{
		private final Map<String, DiscountPolicy> policyMap;
		private final List<DiscountPolicy> policies;
		
		//생성자
		@Autowired
		public DiscountService(Map<String, DiscountPolicy> policyMap,
				List<DiscountPolicy> policies) {
			this.policyMap = policyMap;
			this.policies = policies;
			
			System.out.println("policyMap = "+ policyMap);
			System.out.println("policies = "+ policies);
		}
	}
}

 

 

 

빈 등록과 함께 의존성 주입이 이루어지는데 

먼저 빈등록 코드 설명을 하자면

  • ApplicationContext ac = new AnnotationConfigApplicationContext(DiscountService.class);

이렇게 하면 DiscountService 클래스만 스프링빈에 등록이 되니까 AutoAppConfig.class 도 추가해주기

  • ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);

첫번째 파라미터 AutoAppConfig 클래스는 컴포넌트 스캔을 통해 @Component 어노테이션이 붙은 클래스들을 스프링 빈으로 등록하고

두번째 파라미터 DiscountService 클래스는 직접 스프링 빈으로 등록하는 역할을 한다. 

 

  • public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies)

DiscountService 클래스의 생성자에서 DiscountPolicy 를 의존성 주입이 이루어졌다 

그리고 Map , List 형태로 담는데 

DiscountPolicy 인터페이스를 구현한 여러 클래스( FixDiscountPolicy,RateDiscountPolicy ) 를 Map , List  형태로 담는다. 

 

 

Map , List  형태란?? 

  • Map : policyMap 은 각 구현 클래스의 이름을 key 값으로 하고, 구현 객체를 value로 가진다. ex) {키 = 값, 키 = 값}    
  • List  : policies 는 모든 객체를 리스트 형태로 가져온다. ex) [ ]
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;

 

 

[결과]

 

 

 

 

 

반응형
LIST

 

의존관계 주입 방법 4가지 

  1. 생성자 주입
  2. 수정자 주입 (setter 주입)
  3. 필드 주입
  4. 일반 메서드 주입

 

 

1. 생성자 주입

  • 이름 그대로 생성자를 통해 의존관계를 주입 받는 방법
  • 특징
    • 생성자 호출시점에서 딱 한번만 호출되는 것을 보장한다.
  • 장점
    • 불변 : 두 번이상 호출이 안되기때문에 변하지 않음
    • 필수 : 생성자를 통해 들어온 값이 무조건 있어야 함
  • 생성자가 하나일때는 자동으로 @Autowired가 적용이 된다. (생략가능) 
  • 예시
@Component
public class OrderServiceImpl implements OrderService{
	
	private final MemberRepository memberRepository; 
	
	private final DiscountPolicy discountPolicy; 
	
	//생성자 (생성자가 하나라서 @Autowired 생략가능)
	@Autowired
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
	}
}

 

컴포넌트 스캔할 때 OrderServiceImpl 를 스프링 빈으로 등록이 되면 @Autowired 어노테이션이 부여된 생성자가 있기 때문에 생성자를 호출을 하여 의존성 주입을 한다. 

어떻게? 스프링 빈이 MemberRepositoryDiscountPolicy라는 타입의 빈(Bean)을 찾아서 인자로 받아들여 주입시키고  OrderServiceImpl 빈을 생성

 

즉 스프링은 OrderServiceImpl 빈을 생성할 때 MemberRepositoryDiscountPolicy의 구현체를 찾아와서 해당 인스턴스를 생성자를 통해 주입한다. 

 

 

MemberRepository 구현체 [MemoryMemberRepository]

@Component
public class MemoryMemberRepository implements MemberRepository{ //MemberRepository의 구현체

	
	private static Map<Long, Member> store = new HashMap<>();
	
	@Override
	public void save(Member member) {
		store.put(member.getId(), member);
	}

	@Override
	public Member findById(Long memberId) {
		return store.get(memberId);
	}
}

 

 

 

 

 

2. 수정자 주입 (setter 주입)

 

  • memberRepository , discountPolicy 필드명에 set 붙이고 카멜표기법으로 필드값을 메서드로 통해 직접 수정하기
    • setMemberRepository, setDiscountPolicy
@Component
public class OrderServiceImpl implements OrderService{
	
	private MemberRepository memberRepository; 
	private DiscountPolicy discountPolicy; 
	
	//setter 의존관계 주입
	@Autowired
	public void setMemberRepository(MemberRepository memberRepository) {
		System.out.println("memberRepository = " + memberRepository);
		this.memberRepository = memberRepository;
	}

	@Autowired
	public void setDiscountPolicy(DiscountPolicy discountPolicy) {
		System.out.println("discountPolicy = " + discountPolicy);
		this.discountPolicy = discountPolicy;
	}
 }

 

    * 생성자 필요 없음

[테스트 결과]

 

 

 

  • 특징 (선택, 변경)
    • 선택적으로 의존관계를 주입할 수 있다.
    • @Autowired(required=false) : 원래는 @Autowired 의 기본동작은 주입할 대상이 없으면 오류가 발생하지만 주입할 대상 없이도 동작하게 된다. 
@Autowired(required=false) //의존관계 안해도 됨.
public void setMemberRepository(MemberRepository memberRepository) { }
    • 변경가능성 있는 의존관계 

 

 

 

 

 

3. 필드 주입

  • 이름 그대로 필드에 바로 주입하는 방법 (권장하지 않음)
  • 특징 
    • 코드가 간결해서 외부에 변경이 불가능해 테스트하기가 어렵다.
    • 실제코드와 관련없는 테스트 코드에 사용
  • 예시 
    • memberRepository, discountPolicy 필드앞에 @Autowired 붙이기 
@Component
public class OrderServiceImpl implements OrderService {

    @Autowired
    private MemberRepository memberRepository;
    
    @Autowired
    private DiscountPolicy discountPolicy;
}

 

 

 

 

4. 일반 메서드 주입

  • 일반 메서드를 통해 주입 받을 수 있다.
  • 특징
    • 아무 메서드에 @Autowired 를 붙일 수 있다.
    • 한번에 여러 필드를 주입 받을 수 있다.
    • 그러나 생성자 주입이나 수정자 주입을 대부분 사용하기 때문에 일반 메서드 주입은 일반적으로 사용하지 않는다. 
  • 예시
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;
	}
    
  }

 

 

※ 참고 

OrderServiceImpl 은 스프링 컨테이너가 관리하는 스프링 빈이라서 @Autowired 가 기능한다.
 
( OrderServiceImpl 는 @Component 을 붙였기 때문에 스프링 컨테이너에 의해 빈으로 관리하는 클래스다. )
 
일반 클래스는 @Autowired 를 적용해도 아무 동작을 하지 않는다.

 

 

반응형
LIST

 

원래 스프링 빈을 직접 등록할때 방법은 두가지 방법이 있다.

  • 자바코드에서 @Bean 
  • xml 에서 <bean> 

이렇게 등록하면 수십, 수백개를 일일이 등록해야하는 귀찮음과 누락하는 문제점이 있다.

그래서 스프링 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 '컴포넌트 스캔' 이라는 기능을 제공한다.

 

 

 

 

컴포넌트 스캔하는 AutoAppConfig 클래스로 테스트해보자

기존의 설정정보인 AppConfig 대신해서 테스트용 설정정보 AutoAppConfig 클래스를 만들기

 

[기존의 AppConfig]

@Configuration
public class AppConfig {

	@Bean
	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}
	
	@Bean
	public MemoryMemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}
	
	@Bean
	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(), discountPolicy()); //discountPolicy()로 대체해주기
	}
	
	@Bean
	public DiscountPolicy discountPolicy() {
//		return new FixDiscountPolicy();  // 1만원이든 2만원이든 천원 할인적용
		return new RateDiscountPolicy(); // 1만원 천원할인, 2만원이면 2천원할인
	}
}

 

 

 

[테스트용 AutoAppConfig]

@Configuration //설정정보에 붙이면 싱글톤 유지
@ComponentScan(
	excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

}

 

 

AutoAppConfig 에서는 @Bean 을 사용하지 않고 빈을 등록할 수 있다. 

그렇기 때문에 기존의 AppConfig 설정정보는 사용하지 않아서 제외시켜주자

 

@ComponentScan

 

  •   1. @Component 가 붙은 모든 클래스를 찾아(스캔해서) 자동으로 스프링 빈으로 등록해주는 어노테이션
  •   2. 기존의 AppConfig 설정정보는 사용하지 않아서 제외시켜준다.
    • @ComponentScan(
       excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
    • @Configuration이 붙은 Appconfig는 @Bean을 써서 스프링 빈을 수동등록 해놨기 때문에 @ComponentScan 에서 제외시킨다. 
    • 컴포넌트 스캔을 사용하면 @Configuration 이 붙은 설정 정보도 자동 등록해주기 때문에 excludeFilters 를 써서 제외시킨다.

 

근데 왜 컴포넌트 스캔은 @Configuration 도 스캔하는 것일까??

 

 

 @Configuration 이 컴포넌트 스캔의 대상이 된 이유

@Configuration 소스코드를 보면 @Component 가 붙어있기 때문! 

 

 

 

 

 

 

이제 AutoAppConfig 에서 @Bean 을 쓰지않고 어떻게 스프링 빈으로 등록을 할 수 있을까?

@Component 가 붙은 모든 클래스를 스캔해서 스프링 빈으로 등록해주기 때문에 각 클래스가 컴포넌트 스캔의 대상이 되도록 @Component를 붙여준다.  

 

** 어떤 클래스에 @Component 를 붙이나?

AppConfig 에서 @Bean이 붙은 메서드의 반환되는 객체의 타입이 스프링 빈으로 등록이 되니까 MemberServiceImpl, MemoryMemberRepository, OrderServiceImpl, RateDiscountPolicy 의 클래스에 @Component 를 붙이면 된다. 

 

 

MemberServiceImpl 에 @Component 붙이기

@Component
public class MemberServiceImpl implements MemberService{}

 

 

MemoryMemberRepository@Component 붙이기

 

@Component
public class MemoryMemberRepository implements MemberRepository{}

 

 

OrderServiceImpl에 @Component 붙이기

@Component
public class OrderServiceImpl implements OrderService{}

 

 

RateDiscountPolicy에 @Component 붙이기

@Component
public class RateDiscountPolicy implements DiscountPolicy{}

 

 

 

 

  • 스프링 빈의 기본 이름은 클래스명을 사용하는데 맨 앞글자를 소문자로 바꿔서 사용한다.
    • 빈 이름 MemberServiceImpl > memberServiceImpl 변경
    • 직접 빈이름을 지정 : @Component("memberServiceImpl2") 

 

 

 

그렇다면 이제 의존관계 주입은 어떻게 할것인가??

기존의 AppConfig의 의존관계 주입은 memberService 는 memberRepository 를 의존관계 주입을 했는데 

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

 

 

AutoAppConfig는 아무것도 없다 어떻게 의존관계 주입을 해야할까? @Autowired 를 사용하여 자동주입을 하면 된다.

@Configuration //설정정보에 붙이면 싱글톤 유지
@ComponentScan(
	excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

}

 

 

 

 

 

@Autowired
의존관계를 자동으로 주입해주는 어노테이션

 

 

  1. @Component 붙은 클래스에서 의존관계 설정을 해주면 된다.  

이전에는 AppConfig에서는 @Bean 을 통해 직접 설정정보를 작성했고 의존 관계도 직접 연결했으나 

이제는 의존관계 주입을 @Component 붙은 클래스에서 생성자에 @Autowired를 붙여주면 된다. 

 

MemberServiceImpl @Autowired 추가

@Component
public class MemberServiceImpl implements MemberService{

	private final MemberRepository memberRepository;
	
	@Autowired 
	public MemberServiceImpl(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}

 

 

그 외 

OrderServiceImpl 클래스에 @Autowired 추가하기

@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;
	}
}

* @Autowired 는 생성자에서 여러 의존관계도 한번에 주입받을 수 있다.

 

 

 

 

@Autowired 동작 설명

 

  2.  생성자에 @Autowired 를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아 주입한다.

 

@Autowired 
public MemberServiceImpl(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}

 

어떻게?

  • MemberRepository 타입과 일치하는 스프링 빈을 찾아준다.
    • ac.getBean(MemberRepository.class) 와 같은 동작이라고 보면 된다.
  • MemberRepository 인터페이스라서 해당 인터페이스를 구현한 MemoryMemberRepository 빈을 찾아 MemberServiceImpl생성자에 넣어준다 
  • 이로써 MemberServiceImpl MemoryMemberRepository 사용할 수 있게된다. 

 

 

+ 만약 생성자의 파라미터에 주입하는 의존관계가 많다면??

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

 

  • OrderServiceImpl 생성자처럼 파라미터가 많아도 스프링 컨테이너에서 다 찾아서 자동으로 주입한다. 

 

 

 

 

테스트 했을때 로그를 보면 ComponentScan 이 잘 작동한것을 볼 수 있다.

public class AutoAppConfigTest {

	@Test
	void basicScan() {
		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
		
		MemberService memberService = ac.getBean(MemberService.class);
		Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
	}
}

 

[결과]

 

 

 


 

@ComponentScan 부가 설명

 

@ComponentScan ( basePackages = "")
탐색 시작 위치 설정하기

 

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

 

  • 이렇게 하면 member 패키지만! 컴포넌트 스캔의 대상이 된다.  다른 패키지는 스캔이 안됨
  • 설정하는 이유?
    • 자바 코드뿐만 아니라 라이브러리까지 모두 스캔하기때문에 오래걸리기 때문에 지정하는것이 좋다.
  • 여러개 시작 위치를 지정가능
    • @ComponentScan(
           basePackages = { "hello.core.member", "hello.core.order" }
  • basePackageClasses : 클래스를 지정해주면 해당 패키지를 탐색 시작 위치가 된다
    • basePackageClasses = AutoAppConfig.class
    • AutoAppConfig 클래스의 패키지를 스캔한다.
  • 시작 위치를 지정을 하지 않는다면? 
    • @ComponentScan 이 붙은 설정정보 클래스의 패키지와 그 하위 패키지들을 모두 스캔한다 (디폴트)
    • 권장방법은 위치 설정(basePackages)을 해주기보다 설정정보 클래스의 위치를 프로젝트 최상단에 두자 

 

참고로 @ComponentScan 을 설정할 필요가 없다 왜냐

스프링 부트를 사용하면  메인 메소드가 있는 CoreApplication 클래스가 자동으로 만들어지는데 

@SpringBootApplication 안에 들어가보면 @ComponentScan 이 이미 있다.

@SpringBootApplication
public class CoreApplication {

	public static void main(String[] args) {
		SpringApplication.run(CoreApplication.class, args);
	}

}

 

 

 

그렇기 때문에 CoreApplication 의 패키지 hello.core부터 스캔을 해서 스프링 빈으로 등록이 되기때문에

굳이 @ComponentScan 을 할 필요는 없다 ^^  

 

 

 

 

반응형
LIST

+ Recent posts