
@는 대체 뭘까? Spring과 JUnit 애노테이션 정리오늘은 오픈소스 개발 프로젝트를 진행하면서 자주 마주친
Java의 애노테이션(Annotation)에 대해 정리해보고자 한다.
프로젝트 코드를 보다 보면 이런 코드들이 계속 등장한다.
@Component
class LsTrDispatcher {
}
@RestController
class LsController {
}
@Configuration
class AppConfig {
@Bean
SimulationEngine simulationEngine() {
return new SimulationEngine(...);
}
}
그리고 테스트 코드에서는 이런 것도 보인다.
@Test
@DisplayName("빈 이름으로 조회하면 예외가 발생한다")
void findBeanByNameX() {
}
Java 문법 자체는 어느 정도 익숙한데
문제는 앞에 계속 붙어 있는
@Component
@RestController
@Configuration
@Bean
@Test
@DisplayName
이 녀석들이었다.
@뒤에 붙어 있는 저 꼬리표들은 대체 뭘까?
처음에는 전부 Spring에서 사용하는 특별한 문법인 줄 알았다.
하지만 공부해보니
@ 자체는 Java의 Annotation 문법이고,
Spring이나 JUnit 같은 Framework가
이 Annotation을 읽어서 각각의 역할을 부여하는 것이었다.
이번 글에서는
Annotation이란 무엇인지
Spring의 주요 Annotation은 무엇인지
Spring Bean은 무엇인지
@Component와 @Bean은 무엇이 다른지
@Configuration은 왜 필요한지
@Test와 @DisplayName은 무엇인지
까지 한 번에 정리해보고자 한다.
Java에서 @로 시작하는 것을
Annotation(애노테이션)이라고 한다.
예를 들어 다음과 같은 것들이다.
@Override
@Deprecated
@Component
@RestController
@Configuration
@Bean
@Test
@DisplayName
애노테이션은 쉽게 말하면
코드에 추가적인 정보나 의미를 붙이는 표시
라고 생각할 수 있다.
예를 들어
@Override
는
"이 메서드는 부모 클래스나 인터페이스의 메서드를 재정의한 거야."
라는 의미를 표현한다.
마찬가지로 Spring에서
@Component
를 사용하면
"이 클래스는 Spring이 관리해야 해."
라는 의미를 전달할 수 있다.
즉 애노테이션 자체가 객체를 만들거나
HTTP 요청을 처리하는 것은 아니다.
해당 애노테이션을 읽는 Framework가
그 의미에 따라 동작하는 것이다.
예를 들어
Spring
→ @Component, @Bean, @Configuration 등을 읽음
JUnit
→ @Test, @DisplayName 등을 읽음
이라고 볼 수 있다.
@Component먼저 @Component부터 살펴보자.
@Component를 정말 쉽게 표현하면 다음과 같다.
"이 클래스의 객체는 Spring이 만들어서 관리해줘."
예를 들어 다음 클래스가 있다고 해보자.
class LsTrDispatcher {
}
일반적인 Java 코드라면 이 객체가 필요할 때
개발자가 직접 객체를 생성할 수 있다.
LsTrDispatcher lsTrDispatcher =
new LsTrDispatcher();
즉,
개발자가 직접 new
↓
객체 생성
↓
개발자가 객체 사용
하는 것이다.
그런데 클래스에 @Component를 붙이면 상황이 달라진다.
@Component
class LsTrDispatcher {
}
Spring은 애플리케이션이 시작될 때
Component Scan 과정에서 이 클래스를 발견할 수 있다.
그리고 해당 객체를 생성해서
Spring Container 안에서 관리한다.
쉽게 표현하면 다음과 같다.
@Component 발견
↓
"이 클래스는 내가 관리해야 하는 객체구나."
↓
LsTrDispatcher 객체 생성
↓
Spring Container에 등록
그러면 개발자가 필요한 곳마다
new LsTrDispatcher();
를 직접 작성하며 객체를 관리할 필요가 줄어든다.
처음 이 개념을 알았을 때는
엄청 편리한데?
이래서 Spring을 쓰는 건가?
라는 생각이 들었다.
물론 Spring을 사용하는 이유가 이것 하나뿐인 것은 아니지만,
객체의 생성과 객체 사이의 관계를 Spring에게 맡길 수 있다는 점은
Spring의 매우 중요한 특징 중 하나다.
여기서 새로운 용어가 하나 등장한다.
바로 Spring Bean이다.
Spring Container가 생성하고 관리하는 객체를
일반적으로 Spring Bean이라고 한다.
예를 들어
@Component
class LsTrDispatcher {
}
가 Spring에 의해 등록되었다면
LsTrDispatcher 객체
는 Spring Bean이 된다.
구조를 단순하게 표현하면 다음과 같다.
Spring Container
│
└── LsTrDispatcher 객체
↑
Spring Bean
즉 지금 단계에서는
Spring Bean
=
Spring Container가 관리하는 객체
라고 이해하면 된다.
@RestController다음은 @RestController다.
@RestController
class LsController {
}
이것도 Spring이 해당 클래스를 관리하도록 만드는
Annotation 중 하나다.
하지만 @Component보다 추가적인 역할이 있다.
쉽게 표현하면 다음과 같다.
"이 클래스는 Spring이 관리하면서 HTTP 요청도 처리하는 REST Controller야."
예를 들어 다음 코드가 있다고 해보자.
@RestController
class LsController {
@GetMapping("/orders")
public String orders() {
return "주문 목록";
}
}
클라이언트가
GET /orders
라는 HTTP 요청을 보낸다고 해보자.
그러면 Spring이 해당 요청을
적절한 Controller 메서드에 연결한다.
사용자 / 클라이언트
↓
HTTP 요청
↓
@RestController
LsController
↓
orders()
↓
"주문 목록"
↓
HTTP 응답
즉 @RestController는
외부의 HTTP 요청이 Spring 애플리케이션으로 들어오는
입구 역할을 하는 클래스에 주로 사용한다.
@Component와 @RestController의 관계여기서 조금 헷갈렸다.
@Component
class LsTrDispatcher {
}
도 Spring이 관리하고,
@RestController
class LsController {
}
도 Spring이 관리한다.
그렇다면 둘의 차이는 뭘까?
핵심은 역할이다.
@Component
→ 일반적인 Spring 관리 Component
@RestController
→ Spring이 관리하는 Component
+ REST API 요청 처리 역할
실제로 @RestController는
@Controller
+
@ResponseBody
의 의미를 결합한 Annotation이다.
그리고 @Controller 역시
Spring의 Component 계열 Annotation이다.
따라서 지금 단계에서는
@RestController도 Spring Bean으로 관리되지만,
REST API Controller라는 추가적인 역할을 가진다.
라고 이해하면 된다.
@Bean다음은 @Bean이다.
@Bean
SimulationEngine simulationEngine() {
return new SimulationEngine(...);
}
쉽게 표현하면 다음과 같다.
"이 메서드가 반환하는 객체를 Spring Bean으로 등록해."
처음 봤을 때는 이런 생각이 들었다.
뭐야?
@Component랑 똑같은 거 아닌가?
결과만 보면 비슷하다.
둘 다 결국 객체를
Spring Container
안에 Bean으로 등록해서 관리하도록 한다.
하지만 Bean을 등록하는 방식이 다르다.
@Configuration@Bean을 보다 보면
보통 이런 코드가 같이 등장한다.
@Configuration
class AppConfig {
@Bean
SimulationEngine simulationEngine() {
return new SimulationEngine(...);
}
}
처음에는
"
@Configuration은 또 뭐지?"
라는 생각이 들었다.
쉽게 표현하면 다음과 같다.
"이 클래스는 Spring의 설정 정보를 정의하는 클래스야."
즉,
Spring이 관리해야 하는 Bean들을
어떻게 만들 것인지 정의하는 클래스
라고 볼 수 있다.
@Configuration과 @Bean을 같이 보면예를 들어 다음 코드가 있다고 해보자.
@Configuration
class AppConfig {
@Bean
MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
}
이 코드를 하나씩 해석하면 다음과 같다.
@Configuration
→ AppConfig는 Spring 설정 클래스
@Bean
→ memberRepository()가 반환한 객체를 Bean으로 등록
@Bean
→ memberService()가 반환한 객체를 Bean으로 등록
즉 전체적인 흐름은
@Configuration
↓
AppConfig
↓
@Bean 메서드들
↓
객체 생성
↓
Spring Container에 Bean 등록
이라고 볼 수 있다.
@Configuration은 왜 사용할까?여러 객체의 생성 방법과
객체 사이의 관계를 한곳에서 명시적으로 설정하고 싶을 수 있다.
예를 들어
@Bean
MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
를 보면
MemberService
↓
MemberRepository 필요
라는 의존 관계가 코드에 드러난다.
즉 @Configuration 클래스는
애플리케이션의 객체 구성 정보를 작성하는 곳
이라고 생각하면 이해하기 편하다.
@Configuration의 조금 더 중요한 특징여기서 재미있는 부분이 하나 있다.
다음 코드를 보자.
@Configuration
class AppConfig {
@Bean
MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
}
코드만 보면
memberRepository()
를 호출할 때마다
new MemoryMemberRepository();
가 실행될 것처럼 보인다.
그러면
Spring Container에 등록된 MemberRepository 객체
+
MemberService를 만들 때 생성된 또 다른 MemberRepository 객체
처럼 서로 다른 객체가 만들어지는 것은 아닐까?
라는 의문이 생길 수 있다.
하지만 일반적인 @Configuration의 기본 설정에서는
Spring이 이 설정 클래스를 특별하게 처리해서
이미 등록된 Singleton Bean을 재사용하도록 관리한다.
즉 기본적인 Singleton 환경에서는
MemberRepository Bean
↑
│
MemberService
처럼 동일한 Bean을 사용할 수 있다.
이게 내부적으로 어떻게 가능한지는
Singleton
@Configuration
Proxy
CGLIB
같은 개념과 연결된다.
지금 단계에서는 우선
@Configuration은 단순히 설정 클래스라는 표시뿐 아니라,
Spring이 Bean 구성을 제대로 관리할 수 있도록 도와주는 역할도 한다.
정도로 이해하면 충분할 것 같다.
@Component VS @Bean이제 둘의 차이를 제대로 비교해보자.
가장 큰 차이는
Annotation을 어디에 붙이는지를 보면 이해하기 쉽다.
@Component클래스에 붙인다.
@Component
class LsTrDispatcher {
}
Spring은 Component Scan을 통해 이 클래스를 발견하고
객체를 생성해서 관리한다.
@Component
↓
LsTrDispatcher 클래스 발견
↓
Spring이 객체 생성
↓
Spring Container에 등록
@Bean메서드에 붙인다.
@Bean
SimulationEngine simulationEngine() {
return new SimulationEngine(...);
}
Spring은 메서드가 반환한 객체를
Bean으로 등록해서 관리한다.
@Bean
↓
simulationEngine() 실행
↓
SimulationEngine 객체 반환
↓
Spring Container에 등록
개념적으로 보면
SimulationEngine engine =
new SimulationEngine(...);
으로 만든 객체를
Spring Container가 관리하도록 등록
하는 것과 비슷하다.
내가 직접 작성한 클래스라면
클래스에 바로 @Component를 붙일 수 있다.
@Component
class LsTrDispatcher {
}
그런데 내가 직접 수정할 수 없는
외부 라이브러리의 클래스라면 어떨까?
예를 들어
ExternalClient
라는 외부 라이브러리 객체를
Spring Bean으로 등록하고 싶다고 해보자.
해당 라이브러리 코드에 직접
@Component
를 붙일 수는 없다.
이럴 때 다음처럼 사용할 수 있다.
@Configuration
class AppConfig {
@Bean
ExternalClient externalClient() {
return new ExternalClient();
}
}
즉,
@Component
→ 클래스 자체를 Spring이 찾아서 등록
@Bean
→ 개발자가 객체 생성 방법을 직접 정의해서 등록
이라고 이해할 수 있다.
또한 @Bean은 외부 라이브러리뿐 아니라
객체를 만드는 과정에 추가 설정이 필요한 경우
생성 방법을 직접 제어하고 싶은 경우
여러 객체의 관계를 명시적으로 구성하고 싶은 경우
에도 유용하다.
지금까지 살펴본 내용을 정리하면 다음과 같다.
| Annotation | 붙이는 곳 | 역할 |
|---|---|---|
@Component | 클래스 | 클래스를 Spring Bean으로 등록 |
@RestController | 클래스 | REST API Controller 역할 + Spring Bean 등록 |
@Configuration | 클래스 | Spring Bean 설정을 정의하는 클래스 |
@Bean | 메서드 | 메서드가 반환한 객체를 Spring Bean으로 등록 |
쉽게 표현하면 다음과 같다.
@Component
"이 클래스 Spring이 관리해."
@RestController
"이 클래스 Spring이 관리하고
REST API 요청도 처리해."
@Configuration
"이 클래스는 Spring 설정 클래스야."
@Bean
"이 메서드가 만든 객체를 Spring이 관리해."
예를 들어 프로젝트가 다음과 같이 되어 있다고 해보자.
@Component
class LsTrDispatcher {
}
@RestController
class LsController {
}
@Configuration
class AppConfig {
@Bean
SimulationEngine simulationEngine() {
return new SimulationEngine(...);
}
}
Spring Container 입장에서 보면 대략 다음과 같다.
Spring Container
│
├── LsTrDispatcher 객체
│ ↑
│ @Component
│
├── LsController 객체
│ ↑
│ @RestController
│
└── SimulationEngine 객체
↑
@Bean
↑
AppConfig
↑
@Configuration
객체를 Bean으로 등록하는 방식은 서로 다르지만
결국 중요한 것은 하나다.
Spring Container가 애플리케이션에서 사용할 객체들을 생성하고 관리한다.
@가 붙었다고 모두 Spring Annotation일까?여기까지 공부하면서 한 가지 오해했던 부분이 있다.
프로젝트에서
@Component
@Bean
@Configuration
같은 Annotation을 계속 보다 보니
@Test
@DisplayName
도 처음에는 Spring에서 제공하는 Annotation인 줄 알았다.
하지만 아니다.
@Component
@RestController
@Configuration
@Bean
→ Spring
이고,
@Test
@DisplayName
→ JUnit
이다.
즉 같은 Java Annotation 문법을 사용하지만
어떤 Framework가 해석하는지에 따라 역할이 완전히 달라진다.
@Test@Test는 JUnit에서 사용하는 Annotation이다.
쉽게 표현하면 다음과 같다.
"이 메서드는 테스트 코드야. 테스트로 실행해."
예를 들어
@Test
void basicTest() {
System.out.println("test");
}
가 있다고 해보자.
JUnit은 @Test가 붙은 메서드를 찾아서
테스트 실행 대상으로 인식한다.
@Test 발견
↓
JUnit이 테스트 메서드로 인식
↓
테스트 실행
↓
성공 / 실패 결과 출력
@Test가 없으면?다음 코드를 보자.
void basicTest() {
System.out.println("test");
}
메서드는 존재하지만
@Test가 없다.
그러면 JUnit 입장에서는
"그냥 일반 메서드인가 보다."
라고 생각한다.
따라서 테스트를 실행하더라도
이 메서드는 테스트 메서드로 자동 실행되지 않는다.
즉,
@Test
→ JUnit에게 이 메서드를 테스트로 실행하라고 알려주는 Annotation
이다.
@DisplayName@DisplayName도 JUnit에서 사용하는 Annotation이다.
쉽게 표현하면
"테스트 결과 화면에서는 이 이름으로 보여줘."
라고 생각하면 된다.
예를 들어
@Test
void findBeanByNameX() {
}
라는 테스트가 있다고 해보자.
메서드 이름만으로도 어느 정도 의미를 알 수 있지만
테스트 결과 화면에서 조금 딱딱하게 보일 수 있다.
그래서 다음처럼 작성할 수 있다.
@Test
@DisplayName("빈 이름으로 조회하면 예외가 발생한다")
void findBeanByNameX() {
}
그러면 IntelliJ나 JUnit 결과 화면에서
빈 이름으로 조회하면 예외가 발생한다
처럼 읽기 쉬운 이름으로 표시할 수 있다.
@DisplayName을 사용할까?테스트 코드는 단순히 실행만 되는 것이 아니라
무엇을 검증하는 테스트인지 다른 개발자가 바로 이해할 수 있어야 한다.
예를 들어
void findBeanByParentTypeDuplicate() {
}
라는 이름도 의미는 알 수 있다.
하지만
@Test
@DisplayName("부모 타입으로 조회 시 자식이 둘 이상이면 중복 오류가 발생한다")
void findBeanByParentTypeDuplicate() {
}
처럼 작성하면 훨씬 직관적이다.
테스트 결과에서도
✔ 부모 타입으로 조회 시 자식이 둘 이상이면 중복 오류가 발생한다
처럼 볼 수 있다.
@Test와 @DisplayName은 왜 같이 사용할까?보통 다음처럼 함께 사용한다.
@Test
@DisplayName("특정 타입의 Bean을 모두 조회한다")
void findAllBeanByType() {
}
둘의 역할은 완전히 다르다.
@Test
"이 메서드를 테스트로 실행해."
@DisplayName
"테스트 결과에서는 이 이름으로 보여줘."
따라서 @DisplayName만 붙였다고
테스트가 실행되는 것은 아니다.
예를 들어
@DisplayName("테스트")
void test() {
}
만 작성하면
테스트 이름 정보
는 존재하지만
JUnit이 이 메서드를 테스트 메서드로 실행할 근거인
@Test
가 없다.
지금까지 나온 Annotation을 정리하면 다음과 같다.
| Annotation | 제공 주체 | 붙이는 곳 | 역할 |
|---|---|---|---|
@Component | Spring | 클래스 | Spring Bean으로 등록 |
@RestController | Spring | 클래스 | REST API Controller + Bean 등록 |
@Configuration | Spring | 클래스 | Spring 설정 클래스 |
@Bean | Spring | 메서드 | 반환 객체를 Spring Bean으로 등록 |
@Test | JUnit | 메서드 | 테스트 메서드로 실행 |
@DisplayName | JUnit | 클래스 / 메서드 | 테스트 이름을 읽기 쉽게 표시 |
즉 @가 붙었다고 해서
모두 Spring에서 사용하는 것은 아니다.
Java의 Annotation 문법을
각 Framework가 자신의 목적에 맞게 활용하는 것이다.
Spring
@Component
@RestController
@Configuration
@Bean
→ 객체 생성 / 관리 / 웹 요청 / 설정
JUnit
@Test
@DisplayName
→ 테스트 실행 / 테스트 표현
처음에는 Annotation을 볼 때마다
이걸 또 외워야 하나?
라는 생각이 들었다.
하지만 공부하고 나니
한 가지 질문을 먼저 던지는 것이 훨씬 이해하기 좋았다.
"누가 이 Annotation을 읽는가?"
예를 들어
@Component
를 보면
Spring이 읽는다.
↓
Spring Bean으로 등록한다.
라고 연결한다.
@Test
를 보면
JUnit이 읽는다.
↓
테스트 메서드로 실행한다.
라고 연결한다.
즉 단순히
@Component = Bean
@Test = Test
처럼 외우는 것보다
어떤 Framework가
↓
어떤 Annotation을 읽고
↓
어떤 동작을 수행하는가?
를 이해하는 것이 더 중요하다고 느꼈다.
여기까지 이해하고 나니
다음 의문이 생겼다.
예를 들어 LsController가
LsTrDispatcher를 사용해야 한다고 해보자.
그냥 이렇게 하면 되는 것 아닌가?
@RestController
class LsController {
private final LsTrDispatcher dispatcher =
new LsTrDispatcher();
}
Java 문법상 전혀 이상하지 않다.
그런데 Spring에서는 왜 굳이
@Component
class LsTrDispatcher {
}
처럼 객체를 Spring Bean으로 등록하고
Spring Container에게 관리를 맡기는 것일까?
왜
new LsTrDispatcher();
를 직접 하지 않는 것일까?
이 질문을 이해하면
@Component
@Bean
@Configuration
Spring Container
가 왜 필요한지도 자연스럽게 연결된다.
바로 의존성 주입(Dependency Injection, DI) 때문이다.
이번 글에서 정리한 내용을 가장 짧게 압축하면 다음과 같다.
Annotation
→ 코드에 추가적인 의미를 전달하는 Java 문법
Spring에서는
@Component
→ "이 클래스 Spring이 관리해."
@RestController
→ "이 클래스 Spring이 관리하고 REST API 요청도 처리해."
@Configuration
→ "이 클래스는 Spring 설정 클래스야."
@Bean
→ "이 메서드가 만든 객체를 Spring이 관리해."
JUnit에서는
@Test
→ "이 메서드는 테스트야. 실행해."
@DisplayName
→ "테스트 결과에는 이 이름으로 보여줘."
라고 이해할 수 있다.
처음에는 @가 붙은 코드가 계속 등장하니
뭔가 Spring만의 복잡한 문법처럼 느껴졌다.
하지만 결국
Java의 Annotation이라는 공통 문법을 Spring과 JUnit이 각자의 방식으로 활용하고 있는 것
이었다.
그리고 Spring 쪽 Annotation들을 공부하다 보니
결국 하나의 질문으로 이어졌다.
왜 객체를 개발자가 직접 만들지 않고 Spring이 관리하려고 하는가?
이 질문이 다음에 공부할
의존성 주입(DI)의 시작점이다.
다음 글에서는
왜 new를 직접 하면 문제가 생기는지
의존성이 무엇인지
DI는 무엇인지
생성자 주입은 왜 사용하는지
를 이어서 정리해보고자 한다.