본 게시글은 김영한님의 스프링 핵심 원리 기본편을 정리한 글입니다.
우리가 이때까지 의존관계 주입하는 방법으로 지금까지는 우리가 생성자 주입을 사용을 했었는데 사실은 이거 하나만 있는 것은 아니고 다른 여러가지 방법이 있다고 한다.
이렇게 4가지가 있다고 하는데 하나씩 알아보자.
이때까지 우리가 사용했었던 방법이다. 생성자 주입의 장점은 이게 다른 주입 방법들에 대한 예시들을 보면 잘 드러나기에 뒤로가면서 얘기를 하겠다.
우선 생성자 주입의 특성은 다음과 같다.
이렇게 2가지인데 솔직히 처음 이것을 보고는 음... 무슨 말이지? 라는 생각이 들었는데 위에 말했던 것처럼 다른 주입 방법의 예시들을 보게 되면 이 말을 이해할 수 있었으니 뒤로 가면서 더 자세히 알아보자.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// 생성자가 1개이면 @Autowired 필요 없음
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) { // impl 입장에서는 외부에서 의존성 주입 DI
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
생성자가 만약 1개라면 @Autowired를 작성하지 않아도 된다고 한다. 그리고 계속 우리가 봤던 방법이지만 우리가 여기서 기억을 또 해야할 것은 final이라는 키워드를 잘 기억하고 가자.
수정자 주입은 이제 setter를 활용하여 주입을 시켜주는 방법인데 위의 예시와는 당연히 달라지며 이제 다른 코드들도 수정을 조금씩 해줘야 했었다. 그리고 생각할 것이 생성자 주입은 이제 객체를 생성하게 되면 그때 주입을 시켜주지만 setter 주입과 같은 경우는 객체가 생성이 다 되고 난 후에 주입을 시켜줄 수가 있어서 final 키워드는 작성하지 못한다는 점이 있다.
수정자 주입의 특성은 다음과 같다.
우리가 또 생각해 볼 것이 선택적이고 변경이 가능하다는 것... 이것은 어찌보면 OCP를 어기는 것이 될 수도 있는것이 수정은 하면 안되고 확장에만 열려있는 방식을 선호하기에 이것만 사용하기에는 좋은 객체 지향 설계방향이라는 맞지 않을 수 있다.
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
// setter로 지정을 해보면 final 이 되지 않고 그리고 객체가 아직 생성된 후 부를 수 있기 때문에 final 못 씀
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy){
this.discountPolicy = discountPolicy;
}
맞다! 테스트하기 위하여서는 당연한 얘기이겠지만 AppConfig에 있는 설정파일에서 return 값도 조정을 하여야 한다는 것 잊지 말자.
자바에서는 과거부터 필드의 값을 직접 변경하지 않고, setXXX, getXXX 이라는 메서드를 통하여서 값을 읽거나 수정하는 규칙을 만들었는데 이것이 자바빈 프로퍼티 규약이다.
그니까 한 마디로 Setter, Getter를 사용하는 방식이라는 얘기인 것 같다.
필드 주입이다. 어찌 보면 가장 간단하게 작성이 가능하고 개발자 입장에서는 가장 편한 방법일지도 모르지만 단점이 생각보다 있다. 필드 주입의 특성은 다음과 같다.
이제 예시와 테스트 코드를 보면서 어떠한 문제가 있어서 이렇게 사용을 하지 말라고 하신 건지 한 번 확인해보자.
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
어떻게 너무 간단하다. 근데 간단한 만큼 문제도 있다.
@Test DI컨테이너 없이 그냥 하게 되면 주입시켜주는 친구가 없어서 memberRepository에도 없고 DiscountPolicy도 없어서 NPE가 뜸
void fieldInjectionTest(){
OrderServiceImpl orderService = new OrderServiceImpl();
Order itemA = orderService.createOrder(1L, "itemA", 10000);
System.out.println("itemA = " + itemA);
}
주석에서 작성한대로 이렇게 그냥 실행을 시켜버리면 문제가 당연히 일어난다. createOrder할때 이제 뭐 memberRepository도 없고 뭐 DiscountPolicy도 없어서 주입을 시켜줄라고 해도 뭐 어찌할 방법이 없다. 그래서 보게 되면 결국 수정자 주입처럼 주입을 할 수밖에 없다는 사실에 이마를 탁 치고 간다. 그니까 되도록 이 방식은 사용을 하면 안되겠다.
일반 메서드 주입 방식은 되게 뭐라고 해야되지 정말 순수한 물과 같다라고 할까? 뭐가 다른 것인지는 모르겠지만 순수한 느낌이 강하다.
특성
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired // 메서드로 부르는 방식은 거진 생성자 주입이랑 별로 다를게 없음...
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy){
this.memberRepository=memberRepository;
this.discountPolicy=discountPolicy;
}
음.. 생긴 것만 봐서는 생성자 주입이랑 다를게 딱히 없는데? 라는 느낌이 되게 강하다. 그래서 일반적으로 잘 사용을 하지 않는다고 한다.
이게 위에서 하나씩 차근차근 봤는데 생성자 주입을 하지 않는 방법에서는 뭔가 하나씩 문제인 듯 한 것들이 하나씩 있다. 그래서 실제로도 생성자 주입을 많이 사용을 한다고 하는데 우선 가장 중요한 것이 불변 한다는 특성, 실수 방지를 해준다는 차원에서 많이 사용을 하게 되는 것 같다.
불변한다는 특성에서 찾아보게 되면 이전에 봤었던 수정자(Setter) 주입과 같은 것은 이게 외부에서 수정이 가능해버리면 다수의 인원이 다같이 작업을 하는 프로젝트를 진행할때 함부로 접근하여서 고치게 되면 나도 모르게 문제가 발생할 수 있다는 가능성이 있는것이 생각보다 되게 끔찍한 문제인 거 같다. 그리고 심지어 public으로 열어서 동업하는 사람 입장에서도 이것을 하나 하나 다 살펴보고 유지 보수를 해나가야되는 것이 우욱... 하는 느낌이 강하다. 그리고 애초에 생성자 주입은 객체 생성할 때 단 1번만 호출이 되므로 그 다음부터는 우리가 걱정할 필요가 없다는 것이 가장 큰 것 같다.
실수 방지라고 하는 것은 어찌 보면 우리가 오류같은 것을 찾을때 우리가 뭘 실수했는지 잘 찾고 오류들을 수정할 수 있으면 좋지만 다른 방법들은 애초에 꼬이고 꼬이다 보면 좀 발견하기 어려울 수도 있다. 그래서 생성자 주입 방식에서는 애초에 코드에 실수를 하거나 오류가 발생하는 것을 컴파일 오류로 일어날 수 있게 하는 방법이 있다.
Member member = new Member(1L, "memberA", Grade.VIP);
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
뭐 이런 코드가 있다고 하였을때 생성자 주입을 하게 되면 애초에 다른 주입 방법과 달리 코드에서부터 컴파일 오류를 일으켜 어떤 것을 주입을 시켜줘야 했는지 바로 파악할 수 있다 그리고 테스트하는 상황에 맞게 어떠한 것을 주입시켜줄지 본인이 선택도 가능하다.
MemberRepository memberRepository = new MemoryMemberRepository();
DiscountPolicy discountPolicy = new RateDiscountPolicy();
Member member = new Member(1L, "memberA", Grade.VIP);
OrderServiceImpl orderService = new OrderServiceImpl(memberRepository, discountPolicy);
memberRepository.save(member);
다음과 같이 설정도 해줄 수 있다. 그리고 또 중요한 거 위에서 언급하였듯이 final이라는 키워드를 사용할 수 잇다는 것이 너무나도 중요하다. 그래서 우리가 설계를 할 때부터 애초에 필수로 들어가야하는 의존관계에 대한 것을 설정해주지 않는다면 컴파일 오류를 발생시켜서 이런 실수들을 방지시켜준다는 측면이 있다.
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
}
이러면 당연히 오류가 일어나야 하는데 final 작성하지 않으면 또 이런 부분들 실수하는 것을 나중에 가서 알아차릴 수도 있으니 컴파일 오류를 띄워주는 final이라는 키워드를 사용할 수 있는 것도 주요한 특성 중 하나이다.
여기서 언급하여준 것이 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다!라는 말씀을 해주셨는데 그 말도 맞는것이 애초에 나중에 코드 길어져서 오류 잡으려고 막 살펴보다가 NPE터져서 막 잘못 찾는것보다 이렇게 컴파일 오류를 띄워서 빠르게 코드를 수정할 수 있도록 알려주는것이 훨씬 좋을 거 같다.
위에서 주입들을 다 하긴 하였지만 실제로 실무에서 뭐 디폴트 로직같은 것들이 있는데 막 어떨 때는 사용해야하고 어떨때는 사용을 하지 않는 경우도 있다고 한다. 그래서 뭐 빈이 등록이 되지 않아도 동작을 해야할 때가 있다고 한다.
처리 방식은 다음과 같다.
@Autowired(required = false)
로 지정을 하여 수정자 메서드 자체가 호출이 안되게 한다.@Nullable
을 활용하여 자동 주입할 대상이 없으면 null이 입력되도록 한다.Optional <>
을 활용하여 Optional.empty가 입력되도록 한다. @Test
void optionTest(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(OptionTest.class);
}
static class OptionTest{
@Autowired(required = false)
public void setNoBean(Member member){
System.out.println("setNoBean = " + member);
}
@Autowired
public void setNoBean2(@Nullable Member member){
System.out.println("setNoBean2 = " + member);
}
@Autowired
public void setNoBean3(Optional<Member> member){
System.out.println("setNoBean3 = " + member);
}
}
위 코드에서 required = true로 하면 다으모가 같은 오류가 일어난다. Member라는 bean 자체가 없기 때문이다.
옵션 처리를 통하여 다음과 같이 출력이 된다는 것을 알 수 있다.
애초에 setNoBean자체는 Member가 빈으로 등록이 되지 않았으니 호출조차 하지 않았다는 것을 알 수 있다. 이게 required=false를 하였을때 일어나는 효과로 알 수 있었다.