2024. 9. 1. 18:11ㆍFrameWork/SpringBoot
독립 실행형 스프링 애플리케이션
지금까지 만들었던 애플리케이션은 다음과 같은 구조로 작동했다:
- ServletContainer(WebServer) 를 생성하고,
- 그 안에 FrontController를 등록한다.
- 클라이언트가 요청(Request)을 보내면,
- FrontController가 이를 받아서 내부 서블릿 로직에 매핑된 핸들러에게 위임하고,
- 로직은 HelloController 오브젝트에 전달되어 처리된다.
- 그 결과를 다시 FrontController가 받아서 클라이언트에게 응답(Response)을 반환한다.
이 모든 과정을 우리는 직접 객체를 생성하고 등록하면서 처리했다
이제 위에서 사용했던 HelloController를 직접 생성하지 않고 Spring 컨테이너 내부에 Bean으로 등록해서 사용할 수 있도록 바꿔볼 것이다
- ServletContainer: 웹 요청과 응답을 처리하는 컨테이너
- SpringContainer: 애플리케이션 전반의 객체를 관리하는 컨테이너
이제는 FrontController가 HelloController를 직접 생성하거나 참조하지 않고 SpringContainer로부터 HelloController Bean을 주입받아 사용하는 방식으로 바꾸도록 해볼 것이다.
컨테이너란, 여러 객체를 담아두고 필요할 때 꺼내 쓸 수 있도록 객체의 생명주기와 의존성을 관리해주는 일종의 저장소인데 우리가 앞에서 했던 방식은 FrontController를 직접 new 해서 생성한 후에 ServletContainer에 수동으로 등록하는 방식이었다
하지만 SpringContainer는 다르게 동작한다
SpringContainer는 크게 두가지가 필요하다
첫번째는 비지니스 로직을 담고있는 비지니스 오브젝트(POJOs라고 하는 평범한 자바 오브젝트로 특정 프레임워크를 상속받거나 구현하지 않은, 평범한 자바 객체들 말하는데 이래 저래 신경 쓸필요 없이 우리가 직접 만든 애플리케이션 코드를 말하는 것이다.
두번째는 이렇게 만들어진 코드를 어떻게 구성할지에 대한 구성정보를 갖고 있는 Configuration Metadata라는 것으로 어떤 객체를 Bean으로 등록할지, 어떤 방식으로 주입할지를 알려주는 정보로 자바 코드나, XML 설정, 혹은 클래스 어노테이션등으로 정보를 제공한다.
이 두가지를 SpringContainer가 조합해서 내부에 bean이라고 불리우는 오브젝트들을 구성해서 서버 애플리케이션으로 만들어준다.
이 작업을 한번 코드로 만들어보자.
스프링 컨테이너 사용
기존 소스에서 지금 현재 필요하지 않은 내용들은 한번 제거를 해주도록 하자.
package com.example.demo;
import com.example.demo.mainController.HelloController;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import java.io.IOException;
public class SingleDeomoApplication {
public static void main(String[] args){
HelloController helloController = new HelloController();
TomcatServletWebServerFactory tf = new TomcatServletWebServerFactory();
WebServer ws = tf.getWebServer(new ServletContextInitializer() {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.addServlet("Hello", new HttpServlet() {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
String res = helloController.getParam(name);
if("/hello".equals(req.getRequestURI()) && (HttpMethod.GET.name()).equals(req.getMethod())){
resp.setStatus(HttpStatus.OK.value());
resp.setContentType(MediaType.TEXT_PLAIN_VALUE);
resp.getWriter().print(res);
}else if("/user".equals(req.getRequestURI())){
}else{
resp.setStatus(HttpStatus.NOT_FOUND.value());
}
}
}).addMapping("/*");
}
});
ws.start();
}
}
해당 코드에서 /user라는 부분은 처리할 내용이 존재 하지 않기에 지워주고
if("/hello".equals(req.getRequestURI()) && (HttpMethod.GET.name()).equals(req.getMethod())){
resp.setStatus(HttpStatus.OK.value());
resp.setContentType(MediaType.TEXT_PLAIN_VALUE);
resp.getWriter().print(res);
}else{
resp.setStatus(HttpStatus.NOT_FOUND.value());
}
/home으로 들어오는 요청에서 status code를 만드는 헤더를 넣어주는 부분은 보면 status OK에 대한 내용인데 이는 서블릿에서 상태값을 200이외에 다른 값을 설정하는것이 아니라면 명시적으로 작성하지 않더라도 서블릿 컨테이너가 에러가 나지 않는 한에선 200번으로 세팅을 해서 응답을 반환하도록 되어 있기 때문에 일반적으로는 생략을 한다.
if("/hello".equals(req.getRequestURI()) && (HttpMethod.GET.name()).equals(req.getMethod())){
resp.setContentType(MediaType.TEXT_PLAIN_VALUE);
resp.getWriter().print(res);
}else{
resp.setStatus(HttpStatus.NOT_FOUND.value());
}
이렇게만 하면 코드가 깔끔해졌다.
이제 스프링 컨테이너를 만들어서 그 안에 HelloController를 넣어주고 HelloController를 직접 생성(인스턴스화)해서 사용하는 대신에 스프링 컨테이너한테 요청을 해서 가져와 사용하는 방식을 만들어 주려고 하는데 말했다 싶이 FrontController에서 HelloController를 객체로 만들어 사용할 건 아니기에 해당 부분은 제거해주자.
이러면 아래에 이 객체를 사용하던 부분에서 에러가 날건데
이건 이제 차차 해결해보는 방법을 알아보도록 하고 그 전에 먼저 스프링 컨테이너를 한번 만들어보도록 하자.
스프링 컨테이너를 대표하는 인터페이스가 있는데 이 인터페이스는 ApplicationContext이다.
애플리케이션을 구성하는 많은 정보와 작업, 어떤 bean이 들어갈것인지, 리소스에 접근하는 방법, 내부 이벤트를 전달하는 등등의 애플리케이션이라면 필요한 많은 작업들을 수행해주는 오브젝트들이 구현해야하는 것이 ApplicationContext인데 ApplicationContext가 결국은 SpringContainer가 되는 것이다.
ApplicationContext는 여러가지 구현체가 있는데 그 중에 코드를 기반으로 해서 유연하게 빈을 등록하는 것이 가능한 범용 컨테이너가 GenericApplicationContext라는 것이다.(구현체 중에선 XML이나 어노테이션을 통해서 빈을 등록해야하는 구현체들이 있으나 얘는 코드로 빈을 등록할 수 있다는 의미)
가장 상단에 GenericApplicationContext을 생성해주자.
서블릿이였다면 HttpServlet을 사용해서 new로 오브젝트를 만들어 준다음에 addServlet을 통해서 등록을 해줬었다.
스프링 컨테이너는 오브젝트를 직접 만들어서 넣어주는것도 가능하긴 하나, 일반적으로는 어떤 클래스를 이용해서 bean 오브젝트를 생성할 것인지에 대한 정보(메타정보)를 넣어주는 방식으로 구성한다.
GenericApplicationContext의 registerBean을 통해서 bean을 등록을 해주는데 방법이 여러가지가 있지만 그중 registerBean()에 bean이될 클래스의 정보만 .class를 사용해서 해당 클래스를 bean으로 등록해라 라고 지정해주는 방법이 있다.
이렇게 하면 bean 등록이 끝난거다.
이제 스프링 컨테이너(GenericApplicationContext)는 registerBean으로 전달한 정보를 통해서 bean이 어떤 클래스로 구성될지에 대한 정보를 가지게 된 상태로 bean 오브젝트를 만들어 둔 상태는 아니다.
그렇기 bean오브젝트를 만들줘야 사용이 가능한데 이 때 GenericApplicationContext의 refresh() 함수를 호출해주면 스프링 컨테이너가 갖고 있는 구성정보를 이용해서 컨테이너에 실제 bean을 만들어주면서 초기화 작업을 같이 진행한다.
그리고 나서 서블릿 컨텍스트에서 나는 어떤 클래스의 오브젝트를 받아서 사용해야한다고 원하는 오브젝트에 대한 정보를 요청한다.
이때 사용되는 함수가 getBean이라는 함수이다.
이 함수에 bean을 등록했던것 처럼 클래스에 대한 정보를 전달하면 "아 그거 객체로 만들어 줄게!"하면서 객체로 반환해준다.
이를 단순하게 정리해보면
// 컨테이너 생성
GenericApplicationContext context = new GenericApplicationContext();
// Bean 등록
context.registerBean(MyClass.class);
→ 아직 객체 생성은 하지 않고, 어떤 클래스를 사용할지만 등록
// 컨테이너 초기화
context.refresh();
→ 이때 실제로 Bean 인스턴스들이 만들어지고, 의존성 주입 등 초기화 수행
// Bean 사용
context.getBean(MyClass.class);
이러면 언제든 필요할때 스프링 컨테이너가 갖고있는 bean으로 등록된 오브젝트를 가져와서 사용할 수 있게 된다.
여기서 서블릿 컨테이너 쪽에선 HelloController가 어떻게 만들어 졌는지는 신경 쓰지 않고 HelloController가 필요하니까 그냥 가져다 사용한다는 점이 중요한 점임을 알고 넘어가면 될것 같다.
의존 오브젝트 추가
우리는 앞에서 코드를 이용해 스프링 컨테이너(GenericApplicationContext) 를 직접 만들었고 그 안에 Bean으로 사용할 클래스 정보를 registerBean()을 통해 등록한 뒤 refresh()를 호출해서 실제 Bean 객체들을 생성해 컨테이너에 담아 두었다
그런데… 그냥 new로 직접 만들어 쓰는 것과 뭐가 다를까
굳이 스프링 컨테이너까지 써야 해? 그냥 new HelloController() 하면 되는 거 아닌가? 오히려 더 복잡해진 것 같은데...
하지만 스프링 컨테이너는 단순히 객체를 대신 만들어주는 것 그 이상을 해준다
스프링 컨테이너가 의미 있는 진짜 이유는 스프링 컨테이너는 단순한 객체 공장(factory)이 아니라 여러가지 스프링 컨테이너가 할 수 있는 중요한 일들을 이후에 계속 적용 가능한 기본 주고를 짜놓은것이 의미가 있는 것이다.
스프링 컨테이너는 기본적으로 이 안에 어떤 타입의 오브젝트를 만들때 딱 한번만 만든다.
이게 무슨 의미를 가지냐면 스프링 컨테이너가 가지고 있는 오브젝트를 필요로 하는 앞에 여러가지 오브젝트(서블릿 같은 )가 있을 수 있다
우리는 지금 FrontCotnroller Servlet하나만 만들었지만 추가로 Servlet가 있을때 HelloController가 필요하다면 스프링 컨트롤러에게 getBean이란 메서드를 통해서 요청을 해야하는데 그럴 때 마다 새로운 오브젝트를 컨테이너가 만들어서 전달하는게 아니라 처음에 만들었던 HelloController 오브젝트를, 계속 동일한 오브젝트를 return하는 것이다.
그니까 FrontController와 또다른 Servlet이 같은 HelloController 오브젝트를 사용하게 되는 것이다.
우리가 애플리케이션에서 딱하나의 오브젝트만 만들어두고 재사용하게 만드는 방식을 SingleTon 패턴이라고 말한다.
스프링 컨테이너는 이런 싱클톤 패턴을 사용한것과 유사하게 어떤 타입의 오브젝트를 딱한번만 만들어두고 계속 재사용할 수 있도록 만들어주는 기능을 제공한다.
그래서 스프링 컨테이너를 , SingleTon Registry라고도 한다.
이는 싱글톤 패턴을 사용하지 않고도 마치 싱글톤 패턴을 사용하는 것 처럼 궂이 매 요청마다 새로운 오브젝트를 만드는 대신에 한번만 만들어 놨던 것을 계속 재사용하게 해주는 것이다.
HelloController 안에서 하나의 요청을 다 처리하면 좋겠는데 그러지 못하는 경우가 많기에 역할을 따라 오브젝트를 분리해서 만들고 하나의 오브젝트가 기능이 필요하면 다른 오브젝트에게 동작 수행을 도와달라고 만들어 줄 수 도 있다.
원래 Controller라고 만들어 둔것은 유저의 요청사항을 검증하고 이걸 비지니스 로직을 제공하는 다른 오브젝트(보통 MVC에선 service단)한테 요청을 보내서 결과를 돌려받은 다음에 웹 클라이언트한테 어떤 식으로 돌려줄것인지를 결정해주는 역할까지만 하면 된다.
그러면 HelloController는 책임이 훨씬 많이 줄어들 것이다.
일단 SimpleHelloService라는 것을 추가로 만들어서 등록해보도록 하자
먼저 클래스 파일을 하나 만들어주고
내부에 문자열을 반환하는 함수를 하나 선언해주자.
그리고 이제 HelloController에서
이렇게 문자열을 반환하던 부분을 SimpleHelloService의 함수를 호출해서 값을 반환하는 형태로 변경해주자.
함수를 호출 하기 위해선 SimpleHelloService를 객체로 생성해주고 이걸 사용해서 함수를 호출해줘야한다.
객체로 먼저 선언하고
객체를 사용해서 응답을 만들어주자.
이제 컨트롤러 단에서는 사용자의 요청에 대한 검증이 사실상 주된 기능이기 때문에 이 안에서 name이라는 변수로 사용자가 값을 담아 보냈는지를 확인해줘야 한다.
만약 넘어오지 않았다면 에러를 내도록 해서 클라이언트에게 너의 요청은 잘못되었으니까 나는 작업을 수행할 수 없어라고 알려줘야 한다.
그래서 name이 null인지를 체크해서 null 인경우에는 에러를 발생 시킬 것인데 방법은 Objects 클래스에 있는 requireNonNull이라는 함수인데
이 안에 null체크를 위한 변수를 던져주면 null인지를 체크한다.
null이라면 Exception을 던지고 null 아니라면 그 값을 그대로 return한다.
실행해보면 정상적으로 서버가 실행하고 코드가 정상적으로 잘 동작한다.
이러면 클라이언트의 요청에 문제가 없는지만 Controller에서 처리하고 실제 응답을 만들어 반환하는 Service 만들어 구현했다.
이제 스프링 컨테이너를 사용해서 더 개선해보는 작업을 해보자.
'FrameWork > SpringBoot' 카테고리의 다른 글
Gradle (1) | 2025.08.06 |
---|---|
윈도우(WSL)로 SDKMan 설치하기 (0) | 2024.02.06 |