스프링 핵심 원리 - 기본편(정리) 2편

해당 글은 김영한님의 스프링 핵심 원리를 완강하고 난 후, 정리를 하기 위한 글로써 전체적인 내용을 포함하고 있는 것이 아닌 개인적으로 다시 한번 더 정리한 내용들만 포함하고 있습니다. 강의를 듣고자 하시는 분들은 아래 링크를 타시고 수강 하시면 되겠습니다. 

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

해당글은 1편이후에 이어지는 글입니다. 1편이 보고 싶은 분들은 아래 링크를 타고 보시길 바랍니다. 

 

스프링 핵심 원리 - 기본편(정리) 1편

해당 글은 김영한님의 스프링 핵심 원리를 완강하고 난 후, 정리를 하기 위한 글로써 전체적인 내용을 포함하고 있는 것이 아닌 개인적으로 다시 한번 더 정리한 내용들만 포함하고 있습니다.

jamesblog95.tistory.com


# 의존성 주입 방식

객체 의존성 관계 주입 방식에는 총 4가지 방법이 존재합니다. 

  • 생성자 주입
  • 수정자 주입(Setter 주입이라고도 합니다)
  • 필드 주입
  • 일반 메서드 주입

이러한 각각의 주입에 대해 한번 정리 해보겠습니다.

 

생성자 주입

이름 그대로 생성자를 통해서 주입을 받는 형식을 말합니다. 가장 큰 특징으로는 생성자 호출시점 딱 한번만 주입을 받는 형식으로 불변성과, 필수 의존관계가 성립이 됩니다. 

 

private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;


@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
                        @MainDiscountPolicy DiscountPolicy rateDiscountPolicy) {

    this.memberRepository = memberRepository;
    this.discountPolicy = rateDiscountPolicy;
}

 

수정자 주입

setter 메소드를 통해 의존관계를 주입하는 방식을 말합니다. 특징으로는 선택, 변경이 가능한 주입 방식이라는 것 입니다. 

@Component
public class OrderServiceImpl implements OrderService {
     private MemberRepository memberRepository;
     private DiscountPolicy discountPolicy;
     
     @Autowired
     public void setMemberRepository(MemberRepository memberRepository) {
    	 this.memberRepository = memberRepository;
     }
     
     @Autowired
     public void setDiscountPolicy(DiscountPolicy discountPolicy) {
    	 this.discountPolicy = discountPolicy;
     }
}

이러한 setter 주입방식을 자바빈 프로퍼티라고 하는데, 우리가 자바 객체에서 만든 getter와 setter를 활용하여 객체를 사용하는 규칙을 자바빈 프로퍼티라고 이해하시면 되겠습니다.

 

필드 주입

필드 변수에 있는 값에다가 @Autowired라는 어노테이션을 붙여 주게됨으로써 간단하게 주입이 가능한 방식을 말합니다. 해당 방법은 코드가 간결해서 사용이 편하다는 것입니다. 하지만 한번 주입이 된 이후에는 변경이 불가능한 방식으로 테스트나 여러 서비스를 활용해야 하는 서비스에서는 사용이 불가하다는 단점이 있습니다. 그렇기 때문에 해당 방식은 사용을 추천하지 않고 있습니다. 

@Component
public class OrderServiceImpl implements OrderService {
     @Autowired
     private MemberRepository memberRepository;
     @Autowired
     private DiscountPolicy discountPolicy;
}

 

일반 메서드 주입

주입 하는 일반메소드를 만들어서 주입하는 방식을 말합니다. 해당 방식은 여러 필드를 주입 받을 수 있지만 잘 사용하지 않는 방식입니다. 

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

 

 

결론

  • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없습니다.
  • 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안됩니다.(불변해야 한다.)
  • 누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아닙니다.
  • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다라는 것입니다.
  • 생성자 주입 방식을 선택하는 이유는 여러가지가 있지만, 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이라는 것 입니다.
  • 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 됩니다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있습니다.
  • "항상 생성자 주입을 선택해라! 그리고 가끔 옵션이 필요하면 수정자 주입을 선택해라. 필드 주입은 사용하지 않는게 좋다." 만 기억하면 됩니다.

 


# Null Bean 대처 방법

스프링 컨테이너에 등록된 빈을 사용할 때 @Autowired라는 어노테이션을 사용하게 되는데, 해당 어노테이션에도 빈이 존재 하지 않는 경우를 대비 하여 옵션이 존재합니다. 

 

먼저 required라는 옵션은 주입 받아야하는 타입의 빈이 컨테이너에 존재하지 않는 경우, 원래는 예외가 발생하지만 required = false 로 지정하게 되면 오류가 발생하지 않습니다. 

@Autowired(required = false)
public void setNoBean1(Member member) {
 	System.out.println("setNoBean1 = " + member);
}

 

다른 방법으로는 @Nullable 어노테이션을 사용하는 방식입니다. 어노테이션 이름을 보아도 추측이 되듯이 Null입력 가능하게 만들어 주는 어노테이션 입니다. null이 입력이 된 경우 문자열 null로 변환을 하여 주입을 하고 예외를 예방하게 됩니다. 이러한 방식은 서버가 NPE(Null Point Exception)으로 발생하는 문제를 예방 하는데 사용이 됩니다.

@Autowired
public void setNoBean2(@Nullable Member member) {
	 System.out.println("setNoBean2 = " + member);
}

 

그리고 Optional클래스를 활용하여 처리하는 방법인데요. 지네릭스를 통해 타입을 지정을 하면 Optional 클래스가 대신 해당 타입의 객체를 받는데, 해당 객체가 null 인경우 empty라는 문자를 반환합니다. 

@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
 	System.out.println("setNoBean3 = " + member);
}

 


# @RequiredArgsConstructor

이제는 생성자 주입방식에서 좀 더 진화된 방식으로 의존성 관계 주입을 하고 있는데요. 그것은 바로 @RequiredArgsConstructor 어노테이션을 사용하여 주입하는 방식입니다. 기존에 사용하던 생성자 주입방식에서 메소드가 사라진 형태로 아래와 같이 간단하게 사용이 가능합니다. 

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
 	private final MemberRepository memberRepository;
 	private final DiscountPolicy discountPolicy;
}

# 여러개의 빈이 존재할 경우

 

@Autowired

@Autowired는 기본적으로 빈을 주입을 하는데 사용되는 어노테이션이지만, 같은 타입의 빈이 여러개가 존재 하는경우, NoUniqueBeanDefinitionException이 발생합니다. 이러한 경우 어떻게 처리하는지 알아보고자 합니다. 

 

기본적으로 @Autowired는 필드에 적인 타입으로 기준으로 해서 빈을 들고오는 이때 여러 개의 빈이 존재한다면 참조변수 이름으로 빈을 매칭합니다. 

 

아래 코드를 예시로는 DiscountPolicy 타입 기준으로 2개의 빈을 들고오고 그중 rateDiscountPolicy라는 이름의 빈을 주입하게 됩니다.

@Autowired
private DiscountPolicy rateDiscountPolicy

 

@Qualifier

다른 방법으로는 Bean을 등록할 시에 @Qualifier를 사용하여 bean의 이름을 등록하는 방식입니다. 

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

 

이렇게 등록된 빈을 다른 곳에 주입하려고 하면 아래와 같이 사용하면 됩니다. 

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

 

@Primary

@Primary는 우선순위를 정하는 방법으로 @Autowired 시 여러 빈이 매칭되면 @Primary가 붙어 있는 빈을 우선적으로 주입을 합니다. 

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

 

추가로

여기서 @Primary와 @Qualifier중 누가 더 우선순위를 가지는가에 대한 궁금증이 발생하는데, spring에서는 좀 더 구체적인 것을 우선적으로 수행합니다. 즉 빈의 이름까지 지정이 되어져 있는 @Qualifier를 우선 수행합니다. 


# Map과 List 활용

  • Map : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아줍니다.
  • List : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아줍니다. 만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입합니다.

# 빈 생명주기

스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 된다. 따라서, 이러한 준비가 완료되었다는 소통을 하기 위해서 여러가지 메소드들로 소통을 하는데 이때, 의존관계 주입이 되면 초기화콜백을 수행하고, 빈이 소멸되기 전에 소멸콜백을 수행한다. 

 

스프링 빈의 이벤트 라이플 사이클은 아래와 같습니다. 

  • 스프링 컨테이너 생성 
  • 스프링 생성
  • 의존관계 주입
  • 초기화 콜백
  • 사용
  • 소멸전 콜백
  • 스프링 종료 

 

생명주기 관리 인터페이스

InitializingBean과 DisposableBean 인터페이스를 사용하게 되면 생명주기 콜백함수를 override를 할 수 있다. 하지만 이러한 인터페이스는 스프링 전용 인터페이스로 해당 코드가 스프링에 의존적인 코드가 된다는 단점이 있다. 그리고 초기화, 소멸 메소드의 이름을 변경 할 수 없다는 단점과 외부 라이브러리에 적용하지 못하는 문제가 있다. 

 @Override
 public void afterPropertiesSet() throws Exception {
     connect();
     call("초기화 연결 메시지");
 }
 @Override
 public void destroy() throws Exception {
	 disConnect();
 }

 

@Bean 옵션을 활용한 초기화, 소멸 메소드 지정

생명 주기를 담당하는 다른 방법으로는 @Bean설정에 있는 initMethod 와 destroyMethod를 지정하는 것입니다. 아래와 같이 코드를 쓰고 메소드를 작성하면됩니다. 해당빈을 호출할 때 해당 메소드가 수행 됩니다.

 @Bean(initMethod = "init", destroyMethod = "close")
public void init() {
 System.out.println("NetworkClient.init");
 	connect();
 	call("초기화 연결 메시지");
 }
 public void close() {
	 System.out.println("NetworkClient.close");
 	disConnect();
 }

initMethod는 대체로 이렇게 지정을 하여서 사용을 하지만 destroyMethod의 경우에는 지정을 하지 않아도 잘 수행 되는 경우가 있습니다. 이러한 이유로는 Method 내부에 inferred라는 값이 기본값으로 지정이 되어져 있는데 추정이라는 설정 값으로 자동으로 close와 shutdown이라는 이름의 메소드를 호출하기 때문에 그렇다고 합니다. 

 

@PostContstruct, @PreDestroy

최신 스프링에서 가장 권장하는 방법으로 어노테이션만 붙이면 사용가능한 방식입니다. 해당 인터페이스는 자바에서 제공하는 순수 라이브러리로 스프링에 의존하지 않고 있다는 것이 큰 장점입니다. 

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

# 빈 소코프

빈 스코프는 빈의 범위를 뜻 하는데, 빈이 언제 생성되고 사라지는가에 대한 범위를 말합니다. 대체로 스프링 빈의 경우 싱글톤을 유지하는데 이러한 경우 빈 소코프는 싱글톤이라고 할 수 있습니다. 

 

  • 싱글톤 : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 넘위의 스코프를 말합니다. 
  • 프로토타입 : 스프링 컨터에니는 프로토타입 빈의 생성과 의존관꼐 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프를 말합니다. 
  • 웹 관련 스코프
    • request : 웹 요청이 들어오고 나갈 때 까지 유지하는 스코프
    • session : 웹 세션이 생성되고 종료될 때 까지 유지하느 스코프
    • application : 웹 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

 

@Scope

@Scope 어노테이션을 통해 쉽게 스코프를 지정 할 수 있습니다. 또는 빈 등록시에 사용하여 지정 가능합니다.

@Scope("prototype")
@Component
public class HelloBean {}

@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
	 return new HelloBean();
}

# 프로토타입 스코프 

싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스를 반환하는 반면에 프로토타입의 경우 매번 새로운 객체를 생성하여 반환해주는 것이 가장 큰 차이점 입니다. 

싱글톤
프로토타입

여기서 주의 깊게 봐야 하는 것은 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다는 것입니다. 즉 Destroy 메소드에 관한 작업을 수행하지 않는 다는 것입니다. 


# DL

DL(Dependency Lookup)의 줄임말로 사용자가 컨테이너에서 필요한 객체를 찾는 작업을 말합니다. 아래 코드는 getBean() 메소드를 통해 직접 빈을 찾는 코드입니다. 

@Autowired
private ApplicationContext ac;
    public int logic() {
     	PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
         prototypeBean.addCount();
         int count = prototypeBean.getCount();
     return count;
    }

하지만 이러한 코드를 사용하는 경우에는 의존성이 높아지기 때문에 다른 방식으로 처리하는 코드를 작성해야 하는데 그러한 방법으로는 무엇이 있는지 알아보고자 합니다. 

 

ObjectFactory, ObjectProvider

DL 작업을 대신 해주는 클래스는 바로 ObjecFactory와 ObjectProvider 입니다. 이름에서도 유추 할 수 있듯이 객체를 공급해주는 역할을 하게 되는데요. ObjectProvider에 지정된 타입이 스프링 컨테이너에 존재한다고 한다면 해당 객체를 가지고 있다가 필요시에 제공해주는 역할을 하고 있습니다. 

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

public int logic() {
     PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
     prototypeBean.addCount();
     int count = prototypeBean.getCount();
 return count;
}

# 웹 스코프

웹 스코프는 오로지 웹 환경에서만 동작을 합니다. 그리고 프로토타입과 달리 스코프 종료시점까지도 관리하게 됨으로 종료 메소드가 호출이 된다는 것이 가장 큰 특징입니다. 

 

종류

  • request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
  • session: HTTP Session과 동일한 생명주기를 가지는 스코프
  • application: 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
  • websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프

 

이러한 스코프를 사용하는 이유는 어떠한 요청들이 많이 들어오게 되면 싱글톤으로 사용을 하게 되었을 때 그 요청에 맞는 로그를 남기기 어려움이 있습니다. 그러므로 새로운 request마다 로그를 남기게 함으로써 이후에 문제를 더욱 쉽게 추적할 수 있는 내용을 남기는 것입니다. 

 

웹스코프를 지정을 하게 되면 기본적으로 주입을 받기 위해서 객체가 생성이 되어져 있어야 하는데 알고 있듯이 요청자가 아무도 없을 때는 객체가 없는 상태입니다. 그러므로 이러한 빈 객체를 반환하는 오류를 예방하기 위해서 Provider클래스를 사용하여야 합니다. 

private final ObjectProvider<MyLogger> myLoggerProvider;

 

이러한 코드를 사용함으로써 request scope빈의 생성을 지연할 수 있게 됩니다. 이러한 방식이 가능한 이유는 proxy 패턴을 사용하여 사용 이전에는 가짜 더미를 만들어 두어서 제공하게 함으로써 오류를 예방 할 수 이었던 것입니다.