4월이 되고 일병4호봉이 되었다.
그 기념 오늘은 스프링 공부를 하려 한다. (???)
사실 위 이유는 농담이고, 요즘 nodejs, 백준 알고리즘 공부로 인해 스프링부트 공부를 많이 진행하지 못했다.
스프링 핵심원리 기본편 강의를 4월 중순까진 완강하고
5월부터는 spring security를 이용한 로그인 기능을 구현하거나, crud 기능을 응용해서 게시판 기능을 만들거나, 아니라면 크롤링을 통해 유저의 맞은 문제 리스트를 보여주는 기능을 구현하든지 하고 싶다.
사실 이런 내용은 나중에 따로 내 일기장 글에 포스팅하면 되므로 여기서 마치고 본론으로 어서 들어가보자.
인프런 김영한님의 스프링 핵심 원리 강좌를 수강하고
제 개인적인 공부를 복습 겸 포스팅한 내용입니다.
틀린 내용은 댓글로 피드백 부탁드립니다 :)
빈 생명주기를 왜 알아야 하지?
처음에 나도 이런 생각을 많이 했었다. 아직까지도 약간은 이 생각을 지울 수 없기도 하다.
왜냐하면 개발을 직접 하면서, 그리고 실무적으로 큰 프로젝트를 진행하면서 중요성을 느낄 수 있기 때문이다.
그러나 일단 인강을 들으면 이유를 어렴풋이나마 알게 되는데,
바로 객체의 초기화와 종료 작업을 안전하게 진행할 수 있기 때문이다.
???: 객체의 초기화와 종료작업? 객체의 초기화는 그냥 생성자 주입으로 하면 되는 것 아닌가? 그리고 종료는 어차피 스프링 컨테이너가 알아서 해주는 것이 아닌가?
먼저 객체 초기화가 필요한 이유를 설명하겠다.
생성자 주입으로 객체를 생성하는 작업은 모두 알고 있을 것이다.
그런데, 이 생성자 주입으로 객체를 생성하는 작업은 말 그대로 객체를 생성하는 데에만 집중하도록 해야 한다. Solid 원칙의 SRP (단일 책임 원칙)이 기억나는가? 하나의 클래스는 하나의 책임을 가진다는 뜻이다. 물론 생성자는 메소드이지만, 이 생성자에서는 말그대로 객체 생성 및 그에 따른 메모리 할당과 기초적인 변수 세팅만 해주는 것이 유지보수에 더 도움이 된다고 한다. 따라서 생성자에서는 기본 세팅만 해주고, 그 외의 추가적인 작업은 생성자에서 메소드를 호출하는 식으로 진행하거나, 아예 다른 데에서 메소드를 호출하도록 한다. 생성자 내에서 작은 것부터 큰 작업변수까지 모두 세팅해주거나 작업하지 않는다. 데이터베이스 커넥션 풀이나 네트워크 웹서버 소켓에 연결하여 url 등 모든 것을 미리 생성자 주입으로 세팅하는 것은 유지보수 측면에서 더 힘들어진다고 한다.
스프링 컨테이너의 종료에 대한 글은 아래 질문글을 참고하면 좋을 듯 하다.
www.inflearn.com/questions/91767
스프링 컨테이너를 직접 생성했을 때엔 close로 종료 작업을 하는 것이 옳다.
그러나, 우리는 스프링 부트를 이용하여 웹서버와 연결하는 일이 많기 때문에 평상시에 종료작업을 별도로 실행해주지 않아도 스프링부트가 직접 종료를 안전하게 실행해준다고 한다.
객체의 초기화가 필요한 이유 (코드를 보면서 설명)
?? 아니 빈 생명주기를 알려줘야지, 뭔 뚱딴지같은 소리냐?
예제 코드를 진행하다보면 빈 생명주기가 필요한 이유를 더 명확히 알 수 있게 되고, 그에 따라 자동적으로 빈 생명주기 또한 파악할 수 있을 것이다. 그래서 코드를 보면서 설명하려고 한다.
(사실 강의에서도 이 순으로 진행했고, 나 또한 위와 같은 생각을 가지고 있었다. 심지어, 빈 생명주기는 이전 강의에서도 김영한님께서 여러번 언급하셨지만, 중요성을 못느낀 내가 매번 까먹고 있었던 것이다...)
웹서버와 연결된 상태에서, url로 request가 들어왔을 때의 상황을 가정하자.
실제로 웹서버를 연결한 상황은 아니고, url이 문자열로 주어지는 코드를 작성하여 웹서버와 연결됐다고 가정해보는 코드이다.
package hello.core.lifecycle;
public class NetworkClient {
private String url;
public NetworkClient(){
System.out.println("생성자 호출 = "+url);
connect();
call("초기화 연결 메시지");
}
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);
}
}
이제 연결이 잘 되는지 테스트하기 위한 테스트 코드를 작성해보자.
아래와 같이 작성했다고 하자.
package hello.core.lifecycle;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class BeanLifeCycleTest {
@Test
public void lifeCycleTest(){
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
NetworkClient client=ac.getBean(NetworkClient.class);
ac.close();
}
@Configuration
static class LifeCycleConfig{
@Bean
public NetworkClient networkClient(){
NetworkClient networkClient=new NetworkClient();
networkClient.setUrl("https://hello-spring.dev");
return networkClient;
}
}
}
LifeCycleConfig 클래스가 AppConfig클래스이므로 @Configuration 어노테이션을 등록해주었다.
그리고 위에서 만든 NetworkClient클래스를 스프링 빈으로 등록하기 위해 @Bean을 해주었다.
참고로 테스트코드이기 때문에 DI 방법은 크게 신경쓰지 않아도 된다.
그리고 주의할 점은
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
가 아닌
ApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
로 코드를 작성한 후, ac.close();를 작성하면 빨간줄로 컴파일 에러가 뜰 것이다.
ApplicationContext 인터페이스에선 close 메소드가 포함돼있지 않다. 위에서 언급했듯이 실제로 따로 스프링 컨테이너 종료를 호출할 일이 많지 않기 때문에, 이를 상속받고 있는 ConfigurableApplicationContext 클래스의 메소드를 가져오거나, 아니면 이것 또한 상속받고 있는 (우리가 매우 잘 알고 있는 클래스인) AnnotationConfigApplicationContext 클래스의 메소드를 가져오면 된다.
위 테스트코드의 결과는 아래와 같다.
생성자 호출 = null
connect: null
call: null message: 초기화 연결 메시지
당연하다. 생성자로 객체가 생성이 된 후, setUrl 메소드로 url을 지정해주기 때문이다.
참고로, 생성자 내에 connect() 함수와 call() 함수를 호출하여 작업을 맡긴다.
이후 바로 안전하게 종료가 되기 때문에 따로 url 지정이 안되는 것이다.
스프링 빈 생명주기
위와 같이 생성자가 호출이 되고 난 후, 외부에서 추가적으로 세팅을 해주는 경우가 많기 때문에
객체의 초기화를 할 일이 빈번하다고 하다.
위와 같은 이유로 스프링 빈은 간단하게 아래와 같은 생명주기를 가진다.
스프링 컨테이너 생성 -> 스프링 빈 생성 / 객체 생성 -> 의존관계 주입
-> 초기화 콜백 -> 사용 -> 소멸 전 콜백 -> 스프링 종료
단, 생성자 주입은 객체가 생성되는 동시에 주입이 되므로 예외이다. (예외라고 보기에도 애매하긴 하다)
초기화 콜백을 통해 개발자가 의존관계 주입이 완료된 상태임을 확인할 수 있고, 이 때 초기화 작업을 진행해야 함을 알 수 있다.
또한 소멸 콜백을 통해 스프링이 종료되기 전인 상태임을 개발자가 확인할 수 있고, 이 때 종료작업을 안전하게 진행할 수 있다.
위 생명주기 콜백을 스프링은 다음과 같은 3가지 방법으로 지원한다.
- 인터페이스(InitializingBean, DisposableBean)
- 초기화 메소드, 종료 메소드 지정
- @PostConstruct, @PreDestroy 어노테이션 지정
참고로 3번 방법이 제일 많이 쓰이고, 1번 방법이 20년이 된 방법이라 거의 쓰지 않는다고 한다.
어노테이션을 좋아하는 나로서는 너무 좋다. (lombok충이라서 더 어노테이션충이 된듯..)
포스팅이 너무 길어지니까 위 세가지 방법은 다음 포스팅에 알아보자.
참고로 1번, 2번은 대충 설명할 것이다. (사실 길게 설명할 것도 없다.)
'JAVA > Spring_Core' 카테고리의 다른 글
[Spring] 스프링 핵심원리 - 기본편 완강 후기 (0) | 2021.04.10 |
---|---|
[Spring] 스프링이 빈 생명주기 콜백을 해주는 방법 (0) | 2021.04.03 |
lombok은 진짜 신세계다... (0) | 2021.03.22 |
[Spring] 스프링 기능들, 특히 컴포넌트 스캔 공부하면서 (0) | 2021.03.11 |
스프링 공부하면서 느낀 점 (0) | 2021.03.11 |