Java의 `@`는 대체 뭘까? Spring과 JUnit 애노테이션 정리

대현·2026년 8월 11일

JAVA 와 Spring

목록 보기
1/3
post-thumbnail

Java의 @는 대체 뭘까? 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은 무엇인지

까지 한 번에 정리해보고자 한다.


먼저 Annotation이란?

Java에서 @로 시작하는 것을
Annotation(애노테이션)이라고 한다.

예를 들어 다음과 같은 것들이다.

@Override
@Deprecated

@Component
@RestController
@Configuration
@Bean

@Test
@DisplayName

애노테이션은 쉽게 말하면

코드에 추가적인 정보나 의미를 붙이는 표시

라고 생각할 수 있다.

예를 들어

@Override

"이 메서드는 부모 클래스나 인터페이스의 메서드를 재정의한 거야."

라는 의미를 표현한다.

마찬가지로 Spring에서

@Component

를 사용하면

"이 클래스는 Spring이 관리해야 해."

라는 의미를 전달할 수 있다.

즉 애노테이션 자체가 객체를 만들거나
HTTP 요청을 처리하는 것은 아니다.

해당 애노테이션을 읽는 Framework가
그 의미에 따라 동작하는 것이다.

예를 들어

Spring
→ @Component, @Bean, @Configuration 등을 읽음

JUnit
→ @Test, @DisplayName 등을 읽음

이라고 볼 수 있다.


1. @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 Bean이다.

Spring Container가 생성하고 관리하는 객체를
일반적으로 Spring Bean이라고 한다.

예를 들어

@Component
class LsTrDispatcher {
}

가 Spring에 의해 등록되었다면

LsTrDispatcher 객체

는 Spring Bean이 된다.

구조를 단순하게 표현하면 다음과 같다.

Spring Container
│
└── LsTrDispatcher 객체
        ↑
     Spring Bean

즉 지금 단계에서는

Spring Bean
=
Spring Container가 관리하는 객체

라고 이해하면 된다.


2. @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라는 추가적인 역할을 가진다.

라고 이해하면 된다.


3. @Bean

다음은 @Bean이다.

@Bean
SimulationEngine simulationEngine() {
    return new SimulationEngine(...);
}

쉽게 표현하면 다음과 같다.

"이 메서드가 반환하는 객체를 Spring Bean으로 등록해."

처음 봤을 때는 이런 생각이 들었다.

뭐야? @Component랑 똑같은 거 아닌가?

결과만 보면 비슷하다.

둘 다 결국 객체를

Spring Container

안에 Bean으로 등록해서 관리하도록 한다.

하지만 Bean을 등록하는 방식이 다르다.


4. @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은 외부 라이브러리뿐 아니라

객체를 만드는 과정에 추가 설정이 필요한 경우

생성 방법을 직접 제어하고 싶은 경우

여러 객체의 관계를 명시적으로 구성하고 싶은 경우

에도 유용하다.


Spring Annotation 네 가지를 비교하면?

지금까지 살펴본 내용을 정리하면 다음과 같다.

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가 해석하는지에 따라 역할이 완전히 달라진다.


5. @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

이다.


6. @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

가 없다.


Spring과 JUnit Annotation을 한 번에 비교해보자

지금까지 나온 Annotation을 정리하면 다음과 같다.

Annotation제공 주체붙이는 곳역할
@ComponentSpring클래스Spring Bean으로 등록
@RestControllerSpring클래스REST API Controller + Bean 등록
@ConfigurationSpring클래스Spring 설정 클래스
@BeanSpring메서드반환 객체를 Spring Bean으로 등록
@TestJUnit메서드테스트 메서드로 실행
@DisplayNameJUnit클래스 / 메서드테스트 이름을 읽기 쉽게 표시

@가 붙었다고 해서
모두 Spring에서 사용하는 것은 아니다.

Java의 Annotation 문법을
각 Framework가 자신의 목적에 맞게 활용하는 것이다.

Spring

@Component
@RestController
@Configuration
@Bean

→ 객체 생성 / 관리 / 웹 요청 / 설정
JUnit

@Test
@DisplayName

→ 테스트 실행 / 테스트 표현

결국 Annotation을 볼 때 무엇을 생각해야 할까?

처음에는 Annotation을 볼 때마다

이걸 또 외워야 하나?

라는 생각이 들었다.

하지만 공부하고 나니
한 가지 질문을 먼저 던지는 것이 훨씬 이해하기 좋았다.

"누가 이 Annotation을 읽는가?"

예를 들어

@Component

를 보면

Spring이 읽는다.
↓
Spring Bean으로 등록한다.

라고 연결한다.

@Test

를 보면

JUnit이 읽는다.
↓
테스트 메서드로 실행한다.

라고 연결한다.

즉 단순히

@Component = Bean
@Test = Test

처럼 외우는 것보다

어떤 Framework가

↓

어떤 Annotation을 읽고

↓

어떤 동작을 수행하는가?

를 이해하는 것이 더 중요하다고 느꼈다.


그런데 왜 Spring이 객체를 관리해야 할까?

여기까지 이해하고 나니
다음 의문이 생겼다.

예를 들어 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는 무엇인지

생성자 주입은 왜 사용하는지

를 이어서 정리해보고자 한다.

profile
도전을 멈추지 않는 개발자

0개의 댓글