상세 컨텐츠

본문 제목

[Spring 해석] 1장. Spring이 시스템을 구성하는 방법 - 2절. 빈을 실체화 시키고 사용하기

프로그래밍 언어/Java spring

by Nested World 2020. 9. 16. 00:27

본문

명세서 준비

앞 절에서 우리는 원하는 클래스를 빈으로 등록하는 '등록 명세서'와, 각각의 빈들을 연결하는 방식을 결정하는 '연결 명세서'를 작성하는 방법을 알아보았다. 하지만 빈들이 실제로 생성되는 시점은 그것이 최초로 사용되는 시점이지, 등록되었을 때가 아니다. 우리는 명세서를 작성했을 뿐, 이것 자체만으로는 아무것도 동작하지 않는다.

이번 절의 목표는 명세서의 내용대로 실제로 작동하는 시스템을 만드는 것이다. 앞에서 만든 명세서를 그대로 사용해도 되겠지만, 똑같은 내용에 익숙해지지 않고 명세서를 읽는 연습을 하기 위해 조금 바꿔보도록 하자. 아래에 새로운 명세서를 하나 보이겠다.

public class AAA {
	public void Func_A() {
		System.out.println("Func_A");
	}
}
@Component
public class BBB {
	public void Func_B() {
		System.out.println("Func_B");
	}
}
@Component
public class CCC {
	
	@Autowired
	AAA aaa;
	
	public void Func_C() {
		System.out.println("Func_C");
		aaa.Func_A();
	}
}
@Configuration
public class ConfigurationClass {
	@Bean
	public AAA aaa() {
		return new AAA();
	}
}

앞 절의 내용을 보았다면, 위의 등록&연결 명세서가 어떤식으로 빈들을 등록하고 연결하기를 원하는지 파악할 수 있을것이다. AAA, BBB, CCC의 3개 빈을 등록했으며, AAA는 구성클래스에 의해, BBB와 CCC는 @Component 라는 스테레오타입 애노테이션을 통해 등록했다. 그리고 CCC클래스는 AAA에 의존성을 가지고있다(AAA를 필요로 한다) 때문에 @Autowired 애노테이션을 통해, AAA빈의 참조를 CCC가 확보할 수 있도록 명세 해주었다.

 

빈을 사용하기

이제 우리는 main 메소드에서 이들 빈을 실제로 사용해 보고자 한다. 당연한 말이지만, 직접 new 키워드를 통해 각각의 객체를 생성해서는 안된다. 이럴 경우 Spring 프레임워크의 도움을 받을 수 없고, 명세서의 애노테이션들(예컨데 @Component와 @Autowired)들은 아무 쓸모가 없다.

그러면 한가지 의문점이 생긴다. 어쨌든 빈을 사용하기 위해서는 이를 실체화 시켜야 할 것이다. 즉, 인스턴스를 만들고 그 참조를 확보해서 main메소드로 시작해 어떤 로직을 처리해야 할 것이 아니겠는가? 그런데 이를 어떻게 처리해야 할까?

결론부터 말하자면, Spring 프레임워크를 사용하는 개발자는 빈들의 '생성'과 '참조 확보'에 신경을 쓰지 않아도 된다. 아니, 그러지 않아야 한다. 그래야 Spring 프레임워크의 개발 방법론을 따르는 것이기 때문이다. 하지만, 어쨌든 모든 프로그램은 main 메소드에서 시작하므로, main메소드에서 빈의 참조를 아무것도 갖고있지 않은 지금은 할 수 있는것이 없다. 즉, 현재는 다음과 같이 Spring 프레임워크와 개발자의 프로그램이 분리되어있는 상황이다.

즉, main 메소드에서 빈들의 참조를 가지고 있지 못한 상황이다. 때문에 그냥 프로그램을 구동하면 아무 일도 벌어지지 않는다. main 메소드에서 new 키워드를 이용해 AAA나 BBB의 인스턴스를 생성하는것은 무의미 하다는것에 주의하자. 이렇게 하면 Spring 프레임워크의 도움을 받을 수 없고 모든 애노테이션이 작동하지 않으며, 당연히 @Autowired 애노테이션으로 명시한 참조들은 null을 가지게 된다.

빈들이 최초로 생성되는 시점은 해당 클래스가 처음 사용되는 순간이라고 했으므로, 그렇다면 main 메소드에서 어떤 빈을 직접 사용할 수 있는 방법이 있으면 Main 메소드와 스프링 프레임워크가 연결된다. 가장 간단한 방법으로는 AAA나 BBB, CCC에 static 메소드를 추가하고 Main에서 그것을 호출하는것을 생각할 수 있다. 여기서는 그렇게 해보도록 하자. 다만 저들 클래스에 직접 static 메소드를 추가하지는 말고, 이러한 매개역할을 수행할 Shower라는 빈을 하나 추가해보자.

@Component
public class Shower {
	@Autowired
	public DefaultListableBeanFactory df;
	
	@Autowired
	public BBB bbb;
	
	@Autowired
	public CCC ccc;
	
	private static Shower instance;
	
	public Shower() {
		if(Shower.instance != null) {
			return;
		}
		Shower.instance = this;
	}
	
	public static void Show() {
		Shower.instance.b();
		Shower.instance.c();
		Shower.instance.show();
	}
	private void show() {
		for(String name : df.getBeanDefinitionNames()) {
			System.out.println(df.getBean(name).getClass().getName());
		}
	}
	private void b() {
		this.bbb.Func_B();
	}
	private void c() {
		this.ccc.Func_C();
	}
}

이렇게 하면 static인 Show메소드가 사용되는 시점에 Spring 프레임워크에 의해 Shower 인스턴스가 생성되어 하나의 빈이 실체화된다. 그리고 Shower에 주입되어있는 BBB와 CCC도 마찬가지로 생성된다. 또한, 이렇게 Spring 프레임워크에 의해 인스턴스가 생겨날 때에도 당연히 생성자가 호출된다. 이를 이용해 생성자의 로직에서 Shower 인스턴스가 스스로의 참조를 자신의 static 로직에서도 사용할 수 있도록 static instance라는 변수에 할당했다.

static 메소드인 Show에서는 이 참조를 이용하여 인스턴스의 b(), c(), show() 메소드를 호출하고 있다. b, c 메소드가 하는 역할은 직관적이므로 넘어가고, show 메소드에서는 현재 등록되어있는 빈들의 정보를 출력해주는 역할을 한다. Shower는 이를 위해 DefaultListableBeanFactory 참조를 주입받았다. 이는 Spring 프레임워크에 의해 자동으로 등록되는 빈이므로, 개발자가 별도로 등록해줄 필요 없이 그저 @Autowired 애노테이션으로 연결만 명시해주면 된다. (앞으로는 이런 빈들을 많이 사용하게 될 것이다)

이제 아래와 같이 main에서 Shower 클래스의 Show메소드를 호출해주면, 전체 시스템의 동작을 확인할 수 있다.

@SpringBootApplication
public class Test4Application {

	public static void main(String[] args) {
		SpringApplication.run(Test4Application.class, args);
		
		Shower.Show();
	}
}

먼저 Func_B와 Func_C가 호출된것을 볼 수 있고, Func_C에 의해 정상적으로 Func_A가 호출된것을 볼 수 있다. 이는 CCC 빈에 AAA 빈 인스턴스가 정상적으로 생성 및 연결되었다는것을 증명한다. (아니라면 여기서 null 포인터 에러가 발생했을 것이다) 뒤이어 현재 로드되어있는 빈들의 리스트가 표시된다. 다른것들은 Spring 프레임워크가 자체적으로 추가한 빈들이며, 우리가 관심을 가지는것은 BBB, CCC, Shower, AAA다. 모두 정상적으로 존재하는것을 확인할 수 있다.

 

실제로 빈을 사용하는 방식

그렇다면 모든 Spring 프레임워크 시스템은 적어도 하나씩의 '매개 클래스'(위의 예시에서 Shower와 같은 역할을 하며 static 메소드를 제공하는 클래스)를 가져야 할까? 이는 언뜻 보아도 번거롭다. 사실 위의 예시는 Spring 프레임워크가 없는 고전적인 환경에 익숙한 우리를 위해, 고전적인 main 메소드와 Spring 프레임워크를 억지로 이어붙여 본 예시일 뿐이다.

Spring 프레임워크는 시스템을 구성하는 컴포넌트들을 컨테이너에 담아 관리한다. 따라서 Spring 프레임워크를 통해 구현된 이상적인 시스템은 모든 구성요소가 컴포넌트, 즉 빈으로 구성되어 있어야 한다. 예를 들어 Http 요청을 받아서 처리하는 하나의 컴포넌트, 그리고 그 컴포넌트로부터 요청을 받아서 처리하는 다른 컴포넌트, 이런 식으로 말이다. 여기에 main 메소드는 필요하지 않다. 

즉, Spring 프레임워크를 제대로 사용한다면 Main 메소드에서 해야 할 일은 '전혀' 없다. "Http 요청을 받아서 처리하는 컴포넌트"를 등록할수는 있더라도, 어쨌든 main에서 이를 실체화 해야 하지 않느냐고 생각할 수 있다. 물론 위에서 살펴본 것들이 Spring 프레임워크의 전부라면 그렇다. 하지만 실제로 Spring 프레임워크는 이보다 훨씬 많은 기능을 제공한다.

우리는 지금까지 어떤 클래스를 빈으로 등록하기 위해 @Component 애노테이션을 사용해왔다. 하지만 이는 Spring 프레임워크가 제공하는 다양한 애노테이션중 극히 일부에 불과하며, 가장 기본적인 형태에 불과하다. Java의 다양한 객체가 다양한 상속을 걸쳐서 결국 Object로 귀결되듯이, Spring 프레임워크의 애노테이션들도 이러한 파생 관계를 가지고 있다. 특정한 애노테이션들이 조합되어 다른 애노테이션을 만들 수 있다는 뜻이다. (심지어 애노테이션을 직접 선언하고 구현하는것도 가능하다)

@Component는 애노테이션은 Java의 Object 클래스처럼 최상위의 애노테이션은 아니지만, 그래도 상위에 해당하는 애노테이션이다. 바꿔말해서 @Component 애노테이션 자체가 많은 기능을 제공하지는 않는다는 뜻이다. Spring 프레임워크는 @Component 애노테이션을 발견하면 해당 클래스를 빈으로 등록할 준비를 하지만, 단지 그게 전부다. 이때문에 위의 예제에서는 AAA, BBB, CCC 빈을 사용하기 위해 Shower라는 불필요한 클래스를 추가했던 것이다.

하지만 보다 복잡한 일을 수행해주는 애노테이션을 사용하면, 실제로 위의 Shower와 같은 '매개 클래스' 없이도 시스템을 작성할 수 있다. 일례로 @Component 애노테이션에서 파생된 @Controller 애노테이션을 들 수 있다. 이는 웹 서버의 컨트롤러를 구현하는데 특화된 애노테이션이며, 이 애노테이션을 클래스에 등록하는것 만으로 웹 서버의 컨트롤러에 필요한 기반 구현이 끝난다. 매우 간단하므로 예시를 들어 보자.

@SpringBootApplication
public class Test5Application {
	public static void main(String[] args) {
		SpringApplication.run(Test5Application.class, args);
	}
}

@Controller
public class MyController {
	@GetMapping("/")
	public String process() {
		System.out.println("proceed");
		return "";
	}
}

놀랍게도, 위의 로직이 전부다. 이것만으로 웹 브라우저에서 http://localhost:8080을 입력하면 process 메소드가 호출되어 콘솔에 "proceed"가 출력되는것을 확인할 수 있다. (단, 아직 뷰 작업을 하지 않았기 때문에, 브라우저에는 에러메시지가 노출된다)

main 메소드에 Spring 프레임워크를 실행하는 기본 로직을 제외하고는 아무것도 없다는 점에 주의하자. 다만 MyController를 빈으로 등록하고 있는데, 이번에는 @Component 대신 위에서 설명한 @Controller 애노테이션을 사용하고 있다. 이 애노테이션을 사용하면 해당 클래스가 말 그대로 MVC 모델의 Controller로써 작동한다. process 메소드의 @GetMapping 애노테이션은 해당 메소드가 "/" 경로의 Get 요청을 처리하게 해준다.

이로써 main에서 MyController의 static 메소드를 호출하는 식으로 MyController를 '사용'하지 않았음에도 MyController가 빈으로써 정상적으로 실체화되고 사용된 것을 확인할 수 있다. 어떻게 이런 일이 가능할까?

process 메소드에 아까 예제에서 보였던 빈 정보를 출력하는 로직을 추가하면, 위와 같은 빈들이 자동으로 추가된 것을 확인할 수 있다. 이를 통해 우리는 @Controller 애노테이션을 추가하면 Spring 프레임워크가 단순히 MyController 클래스를 빈으로 등록하고 끝내는것이 아니라, http 요청을 처리하기 위한 다양한 빈들을 자체적으로 생성한다는 것을 알 수 있다.

여기서 우리는 main 메소드에 별 내용이 없어도 되는 이유를 이해할 수 있다. 저렇게 Spring 프레임워크에 의해 자체적으로 추가된 빈들은 Spring 말 그대로 프레임워크가 만들어내는 것이므로, 궁극적으로는 저 빈들 모두가 Spring 프레임워크를 실행하는 SpringApplication.run 메소드의 호출을 통해 실체화 된 셈이다. 다만 이를 Spring 프레임워크가 숨겨주고 있는 덕분에, 우리는 위의 예에서처럼 억지로 static 메소드를 추가할 필요 없이 적절한 애노테이션을 사용해 빈들을 구현할 수 있다.

 

비교 불가능한 편의성

Spring 프레임워크가 없었다면, 우리는 적어도 3단계의 작업을 해주어야 한다. 가장 먼저 Http 요청을 받는 리스너를 구현하고, 두번째로 컨트롤러를 구현해야 한다. 마지막으로는 이들을 연결해줘야 한다. 즉, 리스너가 컨트롤러의 참조를 갖고 있도록 구현해야 한다. 사실 여기서 중요한 부분은 '컨트롤러' 내부의 로직 뿐이라는 것에 주목할 필요가 있다. 중요한것은 컨트롤러의 비즈니스 로직인데, 그 이외의 것에 너무 많은 시간을 소모해야 한다.

알다시피, Http 요청을 받는 리스너를 만드는 것은 쉬운일이 아니다. 물론 이를 지원하는 다양한 도구가 있지만, 어쨌든 이론상으로 매우 복잡한 일이다. 소켓의 요청을 비동기적으로 처리하기 위해 IOCP나 Epoll같은 복잡한 기술을 알아야 하고(이는 심지어 OS에 종속적이다), 또한 요청을 적절한 스레드에 할당할 수 있는 스레드 풀도 필요하다. 물론 이를 지원하는 도구가 많으므로 이들을 모두 직접 만들 필요는 없다고 하더라도, 골치아픈 일임에는 분명하다.

또한 리스너와 컨트롤러를 연결하기 위해 리스너에 컨트롤러의 참조를 확보하는것도 쉬운 일은 아니다. 리스너에 참조변수를 선언하고 여기에 컨트롤러의 주소를 할당해주기만 하면 되는 간단한 일이지만, 이런식으로 확장된 시스템이 추후 유지보수에 큰 걸림돌이 될 수 있다는 것에는 모두가 동의할 것이다.

하지만 Spring 프레임워크를 사용하는 순간 이 모든 걱정은 사라진다. 개발자는 컨트롤러에서 각 요청을 처리하는, 실제로 시스템에서 가장 중요한 역할을 담당하는 비즈니스 로직에만 집중할 수 있다. @Controller와 @GetMapping이라는 두개의 어노테이션 만으로 위에서 언급한 모든 기술들이 자동으로 충족되기 때문이다.

리스너에 대해서는 아예 신경조차 쓰지 않아도 되며, 리스너와 컨트롤러를 연결하는데도 신경을 쓸 필요가 없다. 연결해주는 방식도 '의존성 주입'패턴이라는 매우 효과적인 패턴을 사용하므로(사실, 강제되므로), 전임자가 주먹구구식으로 시스템을 구현했을까 전전긍긍하지 않아도 된다. 컨트롤러가 바뀌었을 때 리스너까지 뜯어고쳐야 하는 상황은 발생하지 않는다는 뜻이다.

*의존성 주입 패턴이 궁금하다면 따로 찾아보자. 쉽게 이해할 수 있는 디자인 패턴이다.

 

요약

앞선 1절에서는 어떤 클래스들을 빈으로 등록할지, 그리고 그들을 어떻게 연결할지를 명세하는 방법을 공부했다. 그리고 본 절에서는 이들 빈을 실체화시켜서 실제로 작동하는 시스템을 만드는 실습을 했다. 여기까지가 1장의 끝이다. 2장부터는 Spring 프레임워크를 사용해 실제 작동하는 유용한 시스템을 직접 만들어볼 것이며, 이 과정에서 다양한 애노테이션과 Spring 프레임워크가 지원하는 다른 구성 요소들에 대해서 공부할 것이다.

관련글 더보기