Spring으로 코드를 짜면 클래스 위에 @Component라는 어노테이션을 붙이는 일이 자주 있다. 나는 이 한 줄이 왜 생겼는지, 붙이면 무슨 일이 일어나는지, 어떻게 써야 하는지를 정리해 두려고 이 글을 쓴다.
@Component를 알기 위해서는 Bean을 알아야 한다.
@Component가 하는 일은 한 문장이다 — 「이 클래스로 빈(Bean)을 만들어 달라」는 표시다. 그래서 빈이 무엇이고 왜 필요한지부터 봐야 한다.
자바에서는 new 키워드로 객체를 만든다. 작은 프로그램이면 이걸로 충분하다. 그런데 클래스가 수십 개로 늘면 이런 장면이 생긴다.
public class OrderService {
private final MyService myService = new MyService();
}
public class PaymentService {
private final MyService myService = new MyService();
}
OrderService와PaymentService가 각자new MyService()로 자기 몫의MyService객체를 만든다- 그래서 같은 일을 하는 객체가 두 개 생긴다. 메모리를 그만큼 더 쓰고,
MyService가 안에 값을 들고 있다면 두 객체의 값이 따로 논다 - 나중에
MyService의 생성자가 바뀌어 매개변수가 하나 늘면,new MyService()를 쓴 곳을 전부 찾아 고쳐야 한다
이때 OrderService처럼 「돌아가려면 MyService가 있어야 한다」는 관계를 의존성이라고 부른다. 객체를 「관리한다」는 말은 이 일들을 대신 해 준다는 뜻이다 — 객체를 언제 몇 개 만들지 정하고, 만든 객체를 들고 있다가, 그 객체가 필요한 곳에 넣어 준다. 필요한 곳에 넣어 주는 일을 의존성 주입이라고 한다.
스프링은 이 일을 IoC 컨테이너에게 맡긴다. IoC(Inversion of Control, 제어의 역전)는 「객체를 누가 만드느냐」의 주도권이 내 코드에서 스프링으로 넘어갔다는 뜻이다. 이 컨테이너가 만들어서 담아 둔 객체가 빈이다. 덕분에 나는 객체를 만들고 잇는 코드를 덜 쓰고, 업무 규칙을 코드로 옮긴 부분인 비즈니스 로직에 집중할 수 있다.
그런데 컨테이너는 어떤 클래스로 빈을 만들어야 하는지 스스로 알지 못한다. 내가 알려 줘야 한다. 이렇게 「이 클래스로 빈을 만들어 컨테이너에 담아 달라」고 알려 주는 일을 빈을 등록한다고 한다. 등록하지 않은 클래스는 컨테이너가 모르므로 빈으로 만들어지지도, 다른 곳에 넣어지지도 않는다. 문제는 이 등록을 어떻게 하느냐에 있었다.
초창기 Bean을 등록하는 방법의 문제점
초창기 스프링에서는 XML 설정 파일에 빈을 등록했다. 클래스 하나를 빈으로 만들고 싶으면 그 정보를 XML 파일에 한 줄씩 직접 적어야 했다.
문제 상황 (Spring 2.5 이전)
filename: applicationContext.xml
<bean id="myService" class="com.example.MyService"/>
applicationContext.xml은 스프링이 시작할 때 읽는 설정 파일이다. 이 글에서 예로 든 이름이다. 프로그램의main메서드 같은 시작 코드가new ClassPathXmlApplicationContext("applicationContext.xml")처럼 파일 이름을 넘겨 컨테이너를 만들면, 그 컨테이너가 이 파일을 읽는다<bean .../>한 줄이 빈 하나를 등록한다id="myService"는 이 빈을 부를 이름이다class="com.example.MyService"는 빈을 만들 클래스의 전체 이름(패키지 포함)이다. 스프링은 이 문자열로 클래스를 찾아 객체를 만든다
이 방식은 무엇이 빈인지 한 파일에서 다 보여서 명시적이고 직관적이었다. 하지만 이런 문제가 있었다.
- 유지보수의 어려움: 애플리케이션이 커질수록 XML 파일이 길어지고, 어떤 클래스가 어디에 등록돼 있는지 찾기 어려워졌다.
- 타입 안전성 부족:
class="com.example.MyService"는 자바 코드가 아니라 문자열이다. 컴파일러는 자바 파일만 확인하고 XML 안의 글자는 보지 않는다. 그래서MyService의 이름을UserService로 바꾸고 XML을 깜빡하면 컴파일은 통과하고, 프로그램을 실행해 스프링이 그 클래스를 찾으려는 순간에야 오류가 난다. 이렇게 실행 중에 나는 오류를 런타임 오류라고 한다. - 개발 생산성 저하: 빈으로 쓸 클래스를 새로 만들 때마다 XML 파일도 같이 고쳐야 했기 때문에 개발 속도가 느려졌다.
@Component의 등장과 역할
이 문제를 풀려고 스프링 2.5부터 어노테이션으로 빈을 등록하는 방법이 들어왔다. 그중 하나가 @Component다. 근거는 스프링 소스다 — Component.java 파일의 설명 주석에 @since 2.5가 적혀 있다. @since는 「이 버전부터 있었다」를 적는 주석 표시라서, 아래에서도 버전을 말할 때 이 표시를 근거로 댄다.
@Component를 클래스에 붙이면 그 클래스가 빈 등록 대상이 된다. 앞의 XML 한 줄은 이렇게 바뀐다.
import org.springframework.stereotype.Component;
@Component
public class MyService {
// 비즈니스 로직 구현
}
import org.springframework.stereotype.Component;—@Component는 스프링 라이브러리 안의stereotype패키지에 들어 있다. stereotype 패키지는 「이 클래스가 전체 구조에서 어떤 역할을 맡는가」를 나타내는 어노테이션을 모아 둔 곳이다@Component— 이 클래스로 빈을 만들어 달라는 표시다public class MyService— 평범한 클래스다. XML에 따로 적지 않는다
어노테이션은 그 자체로는 아무 일도 하지 않는 표시일 뿐이다. 이 표시를 읽는 쪽은 스프링이다. 시작 코드가 컨테이너를 만들면(XML 때와 같다) 그 컨테이너가 정해진 패키지의 클래스들을 훑어 @Component가 붙은 클래스를 찾고, 그 클래스로 빈을 만들어 컨테이너에 담는다. 이 일을 컴포넌트 스캔이라고 한다. 따로 이름을 주지 않으면 빈 이름은 클래스 이름의 첫 글자를 소문자로 바꾼 myService가 된다(스프링의 AnnotationBeanNameGenerator가 그렇게 짓는다). 어디를 훑을지 정하는 방법은 뒤의 「@Component를 IoC 컨테이너에 등록하려면?」에서 본다.
이렇게 바꾸면 이런 이점이 있다.
- 유지보수 용이성: 빈인지 아닌지가 그 클래스 파일 맨 위에 적혀 있으므로, 클래스마다 XML 파일을 고칠 필요가 없다.
- 타입 안전성 향상: 클래스 이름을 문자열로 적는 곳이 사라진다.
@Component는 클래스 파일 안에 붙어 있어서 클래스 이름을 바꿔도 따라 고칠 곳이 없다. 그 빈을 쓰는 곳은 바로 아래 코드처럼MyService라는 자바 타입으로 가리킨다. 그래서 이름을 바꾸고 쓰는 쪽을 깜빡하면 컴파일할 때 오류가 난다. - 개발 생산성 향상: 클래스 위에 한 줄만 붙이면 빈으로 등록된다.
이제 등록한 빈을 new 대신 받아 쓰는 모양을 보자. 첫 절의 OrderService는 이렇게 바뀐다.
@Component
public class OrderService {
private final MyService myService;
public OrderService(MyService myService) {
this.myService = myService;
}
}
OrderService도@Component를 붙여 빈으로 만든다new MyService()가 사라지고, 생성자 매개변수로MyService를 받는다- 스프링은
OrderService빈을 만들 때 이 생성자를 보고, 컨테이너에 담아 둔MyService빈을 꺼내 넣어 준다.@Autowired는 「이 생성자로 빈을 넣어 달라」는 표시인데, 생성자가 하나뿐이면 붙이지 않아도 스프링이 그 생성자를 쓴다. 생성자가 여럿이고 매개변수 없는 기본 생성자도 없으면, 그중 하나에@Autowired를 붙여 어느 것을 쓸지 알려 줘야 한다(스프링 프레임워크 공식 문서의@Autowired설명)
PaymentService도 같은 모양으로 쓰면 둘은 같은 MyService 빈 하나를 나눠 받는다. 따로 정하지 않으면 스프링은 빈을 컨테이너에 하나만 만들어 계속 쓰기 때문이다. 이 규칙을 빈 스코프라고 하고, 기본값이 singleton(하나만)이다.
MyService의 생성자가 바뀌어도 그 빈을 만드는 건 스프링이므로, OrderService와 PaymentService는 고치지 않는다. 생성자에 매개변수가 늘면 스프링은 그 매개변수도 같은 방법으로 컨테이너에서 찾아 넣는다. 그래서 늘어난 매개변수의 타입도 빈으로 등록돼 있어야 한다. 이것이 「객체 생성과 관리를 맡긴다」의 실제 모양이다.
이름이 왜 @Component일까?
Component는 「구성 요소」라는 뜻이다. 프로그램을 이루는 부품 하나라는 뜻이고, 그 부품이 무슨 일을 하는지는 말하지 않는다. 그래서 @Component는 특정 역할에 묶이지 않고 어떤 클래스에든 붙일 수 있는 가장 일반적인 표시다. 역할을 드러내는 표시는 다음 절에 나오는 어노테이션들이 맡는다.
확장된 어노테이션들
클래스마다 맡는 역할이 다르다. 코드를 역할에 따라 요청을 받는 층, 업무 규칙을 처리하는 층, DB를 다루는 층으로 나누는 방식을 3계층 구조라고 한다. 스프링에는 이 역할을 이름에 드러내는 어노테이션이 따로 있다.
@Controller: 웹 계층, 곧 바깥에서 온 HTTP 요청을 처음 받는 컨트롤러 클래스에 붙인다.@Service: 비즈니스 로직을 처리하는 서비스 계층의 Service 클래스에 붙인다.@Repository: 데이터 접근 계층, 곧 데이터베이스에 데이터를 저장하고 꺼내는 Repository 클래스에 붙인다.
셋 다 빈으로 등록된다는 점에서는 @Component와 같다. 스프링 소스를 열어 보면 Controller.java, Service.java, Repository.java 모두 자기 선언 위에 @Component를 붙이고 있다. 어노테이션도 @interface라는 문법으로 선언하는 타입이라서, 그 선언 위에 다른 어노테이션을 붙일 수 있다. Service.java를 줄여 보면 이런 모양이다.
@Component
public @interface Service {
}
public @interface Service—@Service라는 어노테이션을 선언한다@Component— 그 선언 위에@Component가 붙어 있다. 그래서@Service를 붙인 클래스는@Component를 한 겹 안에 품은 셈이 된다
컴포넌트 스캔은 @Component를 찾을 때 이렇게 한 겹 안에 품은 것까지 본다(스프링 소스의 ClassPathScanningCandidateComponentProvider가 @Component를 찾는 필터를 이렇게 만든다). 그래서 이 셋도 @Component처럼 찾아서 빈으로 만든다. 다른 점은 이름이 역할을 말해 준다는 것이다. 그래서 역할이 위의 셋 중 하나면 그 어노테이션을 붙이고, 어디에도 속하지 않는 클래스에는 @Component를 붙인다.
생겨난 순서는 이름만 보고 짐작한 것과 다르다. 소스의 @since 표시로 보면 @Controller와 @Service는 @Component와 같은 2.5에 들어왔고, @Repository는 그보다 먼저인 2.0에 들어왔다. 「@Component가 먼저 있었고 거기서 셋이 갈라져 나왔다」는 순서가 아니다.
@Component를 IoC 컨테이너에 등록하려면?
@Component를 붙이기만 해서는 빈이 되지 않는다. 스프링에게 어느 패키지를 훑을지 알려 주는 컴포넌트 스캔 설정이 한 번은 있어야 한다. 이 설정을 한 번 해 두면 그 뒤로는 클래스마다 따로 등록하지 않아도 스캔이 알아서 찾는다. 앞에서 「클래스 위에 한 줄만 붙이면 빈으로 등록된다」고 한 것은 이 설정이 이미 있다는 전제다.
설정은 XML로도, 자바 코드로도 할 수 있다.
<context:component-scan base-package="com.example"/>
@Configuration
@ComponentScan("com.example")
public class AppConfig {
}
- XML 쪽은
base-package에 적은com.example패키지 아래를 훑으라는 한 줄이다. 앞의context:는 이 태그가<bean>과 다른 묶음, 곧 스프링의 context 설정 묶음에 속한다는 접두사다. 이 묶음을 쓰려면 XML 파일 맨 위에 그렇게 선언해 둬야 한다 - 자바 쪽은
@ComponentScan("com.example")가 같은 일을 한다.@Configuration은 「이 클래스는 스프링 설정을 담은 클래스다」라는 표시이고,@ComponentScan은 이런 설정 클래스에 붙여 쓰도록 만들어졌다(ComponentScan.java주석) AppConfig의 몸통이 비어 있는 것은 이 클래스가 하는 일이 위에 붙은 어노테이션을 들고 있는 것뿐이라서다. 시작 코드가new AnnotationConfigApplicationContext(AppConfig.class)처럼 이 클래스를 넘겨 컨테이너를 만들면, 컨테이너가 붙은 어노테이션을 읽고 스캔을 시작한다@ComponentScan은 스프링 3.1에 들어왔다(소스의@since 3.1)- 패키지를 적지 않고
@ComponentScan만 붙이면, 이 어노테이션을 붙인 클래스의 패키지부터 그 아래를 전부 훑는다
Spring Boot를 쓰면 이 설정을 직접 쓸 일이 거의 없다. Spring Boot에서 시작 클래스는 main 메서드에서 SpringApplication.run(...)을 부르는 클래스다. 이 클래스에 붙이는 @SpringBootApplication도 앞의 @Service처럼 선언 위에 @ComponentScan을 붙이고 있어서(패키지는 적지 않았다), 시작 클래스가 있는 패키지 아래가 자동으로 스캔 범위가 된다.
결국 XML에는 「여기를 훑어라」 한 줄만 남고, 클래스마다 <bean>을 적던 일이 사라졌다. 이것이 XML로부터 우리를 해방시켜 준 부분이다.
마치며
@Component는 「이 클래스로 빈을 만들어 달라」는 표시이고, 컴포넌트 스캔이 그 표시를 찾아 빈으로 등록한다. 덕분에 클래스마다 XML을 고치던 일과, 문자열로 적은 클래스 이름 때문에 실행 중에야 드러나던 오류가 사라졌다. 나는 앞으로 역할이 분명한 클래스에는 @Controller · @Service · @Repository를, 그 밖의 클래스에는 @Component를 붙인다.
자신만의 철학을 만들어가는 중입니다.
댓글남기기