Forest Gump?

실수에서 다시 배우는 스프링 의존성 (멱등성의 함정) 본문

카테고리 없음

실수에서 다시 배우는 스프링 의존성 (멱등성의 함정)

code1010 2023. 2. 19. 23:52

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'FCMService': Injection of autowired dependencies failed

 

구글 파이어베이스 연동중, 따로 autowired 주입을 한것도 없는데, autowired 의존성 주입이 실패했다는 메세지가 떴다.

Circular reference(순환 참조)의 위험성 때문에, 또 객체를 생성할 때 최초 한번만 호출이 됨으로 불변함을 유지할 수 있다. 그리고 무엇보다 스프링에서 권장하는 의존성 주입이다! 라는 이유로 거의 공식처럼 외우고 사용하고 있었는데 , 최근들어 강의에서도  몇몇 강사분들이 autowired을 사용하는것을 봐서 너무 그 공식에 생각이 매몰되어 있어서  autowired은 그냥 공식처럼 무조건적으로 사용 안하려고 하는것 아닌가 라는 미안한 생각이 들었다.

막상 롬복을 사용하지 않을때는 소스가 오히려 더 길어져서 간결한 코드인 @autowired가 더 편할때가 있지 않을까? 라는 생각도 들고 이번 기회에 그 이유에 대해 더 낱낱히 파헤쳐 미안함을 덜 느껴 보려고 글을 적는다. 

 

생각을 해보니, 처음에 autowired을 주구장창 쓰다가 안쓰게 된 계기는 생각보다 간단했다. @Service 어노테이션으로 a라는 서비스 클래스를 spring bean 에도 등록하고 autowired로 사용 할 클래스 b의 의존성 주입도 걸어줬지만, 막상 a라는 클래스의 메소드를 사용하려고 

선언을 하려고 b클래스에 객체를 생성해주고 실행을 해줬을때, autowired된 a라는 클래스가 null 로 선언되며 사용이 안되는 문제로 하루종일 삽질을 했었다. 해결법은 너무 단순했다. b라는 클래스에 a라는 클래스를 autowired로 선언한 변수를 갖고 온게 아니라, a클래스 자체를 생성자로 생성해서 사용하고 있기에, 해당 b클래스와 생성자 new 선언을 통한 b클래스는 의존성이 맺어져 있지 않아서 autowired한 클래스에서 null을 불러오는 문제가 있었다. 이 가벼운 내 실수 이후로,  더 확실하게  생성 시점에 알 수 있는 방법은 없을까? 라는 생각에 찾아보다가 생성자 주입을 발견한뒤 autowired를 지양하게 됐다. 

 

 그때 생성자 주입을 썼을때는 객체 지향적인 사용 방법이며, "멱등성"을 만족시킨다는 의미가 되게 크게 와닿았다.

멱등성의 의미를 사전에서 빌리면,  같은 연산을 여러번 실행한다고 해도 그 결과가 달라지지 않는 성질을 의미한다고 한다. 말을 듣고 나서 "아 이게 이런 이유로 싱글톤의 특징과 일치해서 쓰는거구나 " 라고만 생각했다. 

.그리고  스프링 런타임중에 주입된 객체의 변경을 방지하기 위해서는, final키워드로 방어처리를 할 수 있게끔 하는것이다.

라는 말만 믿고 final 처리를 공식처럼 하며 지내왔지만 문득 궁금증이 들었다.

 

그렇다면 final처리는 안하면 어떻게 될까? 아예 실행이 안될까? 라는 생각이 들어서 final 키워드를 없애고 실행을 해봤지만 실행이 된다. 다음은 그 결과다. 

 

java.lang.NullPointerException: Cannot invoke "com.api.service.message.MessageService.findAll(String, String, String)" because "this.messageService" is null

 

 

 

어라, 그럼 필드주입이나 생성자 주입때도 로직을 사용하기 전까지는 의존성 주입이 제대로 되었는지 모르고, 로직을 실행시켰을때만 똑같이 메세지가 리턴되는거라면 왜 굳이 생성자 주입을 권장하는가 궁금해서 찾아봤다. 

 

 

애초에 생성자 주입은 스프링이 실행되면서 인스턴스가 생성될 때 의존성 주입이 Bean에 등록이 되고, 이때 final 이 붙은 필드가 초기화가 되면서 정상적이게 ioc컨테이너 bean 등록이 되면서 불변함(aka 멱등성) 이 유지 될 수가 있어서 생성자 주입을 써라! 라는 말이 생겼다고 정리가 됐다.

 

또한 setter/필드 주입은 인스턴스가 생성 된 후에 의존성 주입이 일어나기 떄문에 이 의존성이 스프링이 실행되고 난다음에 멱등성을 유지한다고 볼 수 없기때문에,  싱글톤 라이프사이클을 기본으로 사용하는 스프링에서는 공식문서에서 부터 생성자 주입을 강조하는 이유였다.

 

여기서 헷갈리는게,  스프링 프레임워크를 사용하지 않고 순수 자바로만 작성할때, 생성자 주입의 멱등성이라는 역할이 모호해서 찾아봤는데, 좋은 블로그 글을 발견했다. 

 

여기서는 Spring의 Bean의 개념까지 알아야 이해가 될겁니다. 스프링의 빈이란 상태가 별도로 없는 일종의 로직을 처리하는 인스턴스에 불과합니다. 따라서 멱등성이라고 하는 개념을 만족해야하는 인스턴스만이 스프링 빈으로 등록되어야 합니다.(물론 라이프사이클마다 다르지만.. 싱글톤이라는 기본 속성하에 이야기하겠습니다.)

그렇다면 런타임중에 주입된 객체의 변경을 방지하기 위해서는 자바의 기본문법인 final 키워드로 방어처리를 할 수 있게 되는것입니다. 싱글톤 라이프사이클로 관리하고 있는 스프링 빈이 있다면 절대 변경되어서는 안될 인스턴스인 것 입니다. 그런데 어플리케이션이 런타임되는 과정에서 주입되는 인스턴스가 변경되는 setter를 둔다는건 굉장한 리스크를 갖고 개발해야하는 것입니다. 개발자들에 의해 컨벤션이 setter 주입 메소드는 건들지 않는다고 할지라도 정성적인 시스템과 절대로 호출할 수 없는 물리적인 시스템에는 분명한 한계가 있게 됩니다.

여기서 final 키워드를 기존의 의존주입의 영역에서 이야기하면 사실 애매합니다. 의존성이 스프링 빈으로 관리될지 알 수 없을 뿐더러 의존객체가 인터페이스라면 전략패턴으로 구현된 셋팅에 의해 다르게 설정되어야 되는 요구사항이 필요하다면 setter가 필요하게 되어 인스턴스 구현체가 변경될 수 있어야 하니까요. 이런 구조에서는 final에 의한 immutable이 말이 되지 않습니다. immutable에 대한 이야기는 스프링의 아키텍쳐내에서 라이프사이클이 싱글톤인 스프링 빈 인스턴스를 의존주입할 때만 immutable이라는 키워드를 사용해도 좋지 않나 생각합니다.

 


출처 : https://sas-study.tistory.com/456

이글을 보고 이해가 좀 더 됐다. final을 생성자 주입이면 무조건 붙이는게 능사가 아니라, 스프링 아키텍쳐 내의 기능을 설명할때만 멱등성이란 말이 잘 어울릴 것 같다.

 

 

현재 기본적인 스프링이 싱글톤으로 빈을 생성하는 이유는 스프링이 주로 쓰이는 환경은 수백개의 요청을 받아 처리할 수 있는 높은 성능이 요구되는 환경이기때문에, 매번 요청때마다 새로운 오브젝트를 생성 안하고 관리하려고 싱글톤으로 처리 했지만, 나중 스프링 이후의 다른 개념이 생긴다면 의존성 주입에 멱등성을 만족시켜야 한다라는 어려운 말은 

필요가 없어지지 않을까? 

 

나중에 공부한 내용이 오버라이드 되어 위 내용에 대해 추가 할 부분이 생길때 마다 업데이트 할 예정이다.