[Spring] Annotation

Hood·2025년 1월 9일

Spring Boot

목록 보기
5/15
post-thumbnail

✍ Back-End 지식을 늘리자!

백엔드 개발자를 준비하면서 생긴 궁금증을 정리한 포스트입니다.


들어가기 전

Annotation은 사전적으로는 주석이라는 뜻이지만,
자바와 스프링에서의 Annotation(@)은 코드에 특별한 의미를 부여하는 문법입니다.

스프링 프레임워크에서 어노테이션은 애플리케이션 구성, 빈 등록, 의존성 주입, 요청 매핑 등을 더 간편하게 처리할 수 있도록 도와줍니다.
즉, 프로그램 자체의 실행 로직이라기보다 프로그램에 대한 추가 정보(메타데이터) 를 제공하는 방식이라고 볼 수 있습니다.

어노테이션을 사용하면 설정을 코드에 명확하게 표현할 수 있어 가독성이 좋아지고, 반복되는 설정을 줄이는 데에도 도움이 됩니다.


Annotation을 사용하려면?

어노테이션은 보통 다음과 같은 방식으로 사용합니다.

  1. @ 기호를 사용해 선언합니다.
  2. 클래스, 메서드, 필드, 파라미터 등에 붙여 사용합니다.
  3. 스프링은 실행 과정에서 이 정보를 읽어 필요한 기능을 적용합니다.

즉, 특정 클래스나 메서드에 어노테이션을 붙이면
스프링이 그 의미를 해석해 알맞은 기능을 수행하게 됩니다.


Annotation의 종류

1. 스프링 부트 시작

@SpringBootApplication

@SpringBootApplication은 스프링 부트 애플리케이션의 시작점을 나타내는 대표적인 어노테이션입니다.

보통 메인 클래스에 붙으며,
스프링 부트 애플리케이션을 실행할 때 가장 먼저 보게 되는 어노테이션이기도 합니다.

이 어노테이션 하나로 다음과 같은 기능을 함께 사용할 수 있습니다.

  • 설정 클래스 등록
  • 자동 설정(Auto Configuration)
  • 컴포넌트 스캔(Component Scan)

즉, 스프링 부트 애플리케이션을 실행하기 위한 핵심 어노테이션이라고 볼 수 있습니다.


2. Bean 등록 및 의존성 주입 관련

@Component

@Component는 스프링 빈으로 등록할 클래스를 지정할 때 사용합니다.

@Component
class MyService {
    fun getMessage() = "Hello, Spring Boot!"
}

Bean이란?

Bean은 스프링 컨테이너가 생성하고 관리하는 객체를 의미합니다.
즉, 스프링이 직접 생명주기를 관리하는 자바/코틀린 객체라고 이해하시면 됩니다.

Spring Bean에 대해서는 이후 글에서 더 자세히 다뤄보겠습니다.

@Service

@Service는 서비스 계층 클래스에 사용하는 어노테이션입니다.
주로 비즈니스 로직을 처리하는 클래스라는 의미를 나타낼 때 사용합니다.

@Service
class UserService {
    fun getUser() = "Hood"
}

@Repository

@Repository는 데이터 접근 계층에서 사용하는 어노테이션입니다.
DB에 접근하는 로직을 담당하는 클래스에 붙입니다.

@Repository
class UserRepository {
    fun findUserById(id: Long): String = "User$id"
}

@Autowired

@Autowired는 스프링 컨테이너에 등록된 빈을 자동으로 주입받을 때 사용합니다.
필드, setter 메서드, 생성자 등에 사용할 수 있습니다.

즉, 객체가 직접 의존성을 생성하는 대신
스프링이 필요한 객체를 찾아 주입해주는 방식입니다.

다만 최근에는 생성자가 하나뿐인 경우 @Autowired를 생략하고 생성자 주입을 사용하는 방식도 많이 사용합니다.


3. 웹 계층 (Spring MVC)

@RestController

@RestController는 REST API를 만들 때 사용하는 어노테이션입니다.

이 어노테이션은 @Controller@ResponseBody가 결합된 형태로,
메서드의 반환값을 뷰(View)가 아니라 HTTP 응답 본문(Response Body) 으로 바로 내려줍니다.

즉, REST API처럼 JSON 형태의 응답을 반환할 때 자주 사용합니다.

@Controller vs @RestController

  • @Controller는 주로 View를 반환하는 컨트롤러에서 사용합니다.
  • @RestController는 View 없이 데이터를 바로 응답하는 API에서 사용합니다.

정리하면 다음과 같습니다.

@RestController = @Controller + @ResponseBody

@RequestMapping

@RequestMapping은 특정 URL을 클래스나 메서드에 매핑할 때 사용하는 어노테이션입니다.

어떤 URL 요청을 어떤 컨트롤러가 처리할지 연결해주는 역할을 하며,
클래스 레벨과 메서드 레벨 모두에서 사용할 수 있습니다.

다만 실무에서는 요청 방식을 더 명확하게 표현하기 위해
@GetMapping, @PostMapping, @PutMapping, @DeleteMapping 같은 전용 어노테이션을 더 자주 사용합니다.

@RestController
@RequestMapping("/api")
class ApiController {
    @GetMapping("/greet")
    fun greet() = "Greetings!"
}

@GetMapping, @PostMapping, @PutMapping, @DeleteMapping

이 어노테이션들은 각각 HTTP 메서드에 맞춘 @RequestMapping의 축약형입니다.

즉, 아래와 같은 의미로 볼 수 있습니다.

  • @GetMapping → GET 요청 처리
  • @PostMapping → POST 요청 처리
  • @PutMapping → PUT 요청 처리
  • @DeleteMapping → DELETE 요청 처리
@RestController
class ProductController {

    @PostMapping("/product")
    fun createProduct(@RequestBody product: String) = "Product created: $product"

    @GetMapping("/product/{id}")
    fun getProduct(@PathVariable id: Long) = "Product ID: $id"
}

4. 웹 요청 처리

@RequestParam

@RequestParam은 URL의 쿼리 파라미터 값을 메서드 파라미터에 매핑할 때 사용합니다.

기본적으로 필수값으로 처리되며, 값이 없으면 오류가 발생할 수 있습니다.
필요하다면 defaultValue를 사용해 기본값을 지정할 수 있습니다.

@RestController
@RequestMapping("/api")
class RequestParamController {

    @GetMapping("/greet")
    fun greetUser(@RequestParam name: String) = "Hello, $name!"

    @GetMapping("/sum")
    fun sum(@RequestParam num1: Int, @RequestParam num2: Int) = "Sum: ${num1 + num2}"

    @GetMapping("/optional")
    fun optionalParam(@RequestParam(defaultValue = "Guest") name: String) = "Hello, $name!"
}

요청 예시

  • /api/greet?name=John"Hello, John!"
  • /api/sum?num1=5&num2=3"Sum: 8"
  • /api/optional"Hello, Guest!"

@PathVariable

@PathVariable은 URL 경로의 일부를 변수처럼 받아올 때 사용합니다.

@RequestParam이 쿼리 문자열에서 값을 가져온다면,
@PathVariable은 URL 경로 자체에서 값을 가져온다는 차이가 있습니다.

@RestController
@RequestMapping("/users")
class PathVariableController {

    @GetMapping("/{id}")
    fun getUserById(@PathVariable id: Long) = "User ID: $id"

    @GetMapping("/{userId}/orders/{orderId}")
    fun getOrderDetails(
        @PathVariable userId: Long,
        @PathVariable orderId: Long
    ) = "User $userId's Order $orderId"
}

요청 예시

  • /users/123"User ID: 123"
  • /users/123/orders/456"User 123's Order 456"

@RequestBody

@RequestBody는 HTTP 요청 본문(body)을 Java/Kotlin 객체로 매핑할 때 사용합니다.

주로 POST, PUT 요청에서 많이 사용하며,
JSON 데이터를 DTO에 바인딩할 때 자주 활용합니다.

data class UserDto(
    val name: String,
    val age: Int
)

@RestController
@RequestMapping("/users")
class RequestBodyController {

    @PostMapping
    fun createUser(@RequestBody userDto: UserDto): String {
        return "Created user: ${userDto.name}, Age: ${userDto.age}"
    }
}
요청 예시

POST /users
Content-Type: application/json

{
  "name": "DH",
  "age": 25
}

-> "Created user: DH, Age: 25"

@RequestPart

@RequestPartmultipart/form-data 요청에서
파일과 JSON 데이터를 함께 받을 때 사용하는 어노테이션입니다.

즉, 파일 업로드와 일반 데이터 전송을 동시에 처리하고 싶을 때 유용합니다.

data class ProductDto(
    val name: String,
    val price: Double
)

@RestController
@RequestMapping("/products")
class RequestPartController {

    @PostMapping("/upload")
    fun uploadProduct(
        @RequestPart("product") productDto: ProductDto,
        @RequestPart("file") file: MultipartFile
    ): String {
        return "Uploaded ${productDto.name} with file: ${file.originalFilename}"
    }
}
요청 예시 (POST - multipart/form-data)

Key: "product" -> {"name": "Laptop", "price": 1500.0}
Key: "file" -> laptop.jpg

-> "Uploaded Laptop with file: laptop.jpg"

@RequestHeader

@RequestHeader는 HTTP 요청 헤더 값을 가져올 때 사용합니다.

API 토큰, 사용자 정보, 언어 설정 등을 읽어와야 할 때 자주 사용합니다.

@RestController
@RequestMapping("/headers")
class RequestHeaderController {

    @GetMapping
    fun getHeaderInfo(
        @RequestHeader("User-Agent") userAgent: String,
        @RequestHeader("Authorization", required = false) authHeader: String?
    ): String {
        return "User-Agent: $userAgent, Auth: ${authHeader ?: "No Token"}"
    }
}
GET /headers
User-Agent: Mozilla/5.0
Authorization: Bearer abc123

5. 데이터 및 트랜잭션 관리

@Transactional

@Transactional은 트랜잭션을 관리하고, 작업 중 문제가 발생했을 때 롤백할 수 있도록 도와주는 어노테이션입니다.

예를 들어 계좌 이체처럼 여러 DB 작업이 하나의 작업 단위로 묶여야 하는 경우,
중간에 하나라도 실패하면 전체 작업을 되돌리는 것이 중요합니다.

이럴 때 @Transactional을 사용하면
메서드 내부의 작업을 하나의 트랜잭션으로 처리할 수 있습니다.

@Service
class BankService(val accountRepository: AccountRepository) {

    @Transactional
    fun transferMoney(fromAccount: String, toAccount: String, amount: Double) {
        accountRepository.debit(fromAccount, amount)
        accountRepository.credit(toAccount, amount)
    }
}

6. 예외 및 검증 처리

@ExceptionHandler

@ExceptionHandler는 특정 예외가 발생했을 때 이를 처리할 메서드를 지정하는 어노테이션입니다.

즉, 예외를 한곳에서 모아 처리할 수 있도록 도와줍니다.

@RestControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException::class)
    fun handleRuntimeException(e: RuntimeException) = "Error: ${e.message}"
}

@Valid

@Valid는 요청으로 들어온 데이터의 유효성을 검사할 때 사용합니다.

예를 들어 이름이 비어 있으면 안 되거나,
이메일 형식이 올바른지 검사해야 할 때 함께 사용할 수 있습니다.

data class User(
    @field:NotBlank val name: String,
    @field:Email val email: String
)

@PostMapping("/user")
fun createUser(@Valid @RequestBody user: User) = "User Created: ${user.name}"

7. JPA Annotation

JPAJava Persistence API의 약자로,
자바 객체와 관계형 데이터베이스를 매핑하기 위한 표준 인터페이스입니다.

ORM이란?

객체와 데이터베이스 테이블을 매핑하는 기술을 의미합니다.

JPA를 사용하면 개발자는 SQL을 직접 작성하는 부담을 줄이고,
객체 중심으로 데이터를 다룰 수 있습니다.

@Entity

@Entity는 데이터베이스 테이블과 매핑되는 클래스를 의미합니다.
이 어노테이션이 붙은 클래스는 보통 기본 키를 나타내는 @Id가 필요합니다.

@Entity
data class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    val name: String,
    val email: String
)
@Entity        // User 클래스를 테이블과 매핑
@Id            // 기본 키 지정
@GeneratedValue // 기본 키 자동 생성 전략 지정

@Table

@Table은 엔티티와 매핑될 데이터베이스 테이블 이름을 지정할 때 사용합니다.

@Entity
@Table(name = "users_table")
data class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    val name: String,
    val email: String
)

@Column

@Column은 필드를 데이터베이스 컬럼과 매핑할 때 사용합니다.
nullable, length 같은 제약 조건도 함께 설정할 수 있습니다.

@Column(name = "user_name", nullable = false, length = 50)
val name: String
@Column(name = "user_name") // name 필드를 user_name 컬럼과 매핑
// nullable = false         // null 허용 안 함
// length = 50              // 최대 길이 50

@Id & @GeneratedValue

@Id는 기본 키를 지정하고,
@GeneratedValue는 기본 키 생성 전략을 지정할 때 사용합니다.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0
@Id                      // 기본 키 지정
@GeneratedValue          // 기본 키 자동 생성 전략
// IDENTITY: DB가 자동 생성
// SEQUENCE: 시퀀스 객체 사용
// TABLE: 키 생성용 테이블 사용
// AUTO: DB 환경에 맞게 자동 선택

@Enumerated

@Enumerated는 enum 타입을 컬럼에 매핑할 때 사용합니다.

enum class Role {
    USER, ADMIN
}

@Entity
data class User(
    @Id @GeneratedValue
    val id: Long = 0,
    @Enumerated(EnumType.STRING)
    val role: Role
)
@Enumerated(EnumType.STRING) // USER, ADMIN 문자열 그대로 저장

@OneToOne, @OneToMany, @ManyToOne, @ManyToMany

1. @OneToOne

하나의 엔티티가 하나의 엔티티와 연결되는 관계입니다.

@Entity
data class User(
    @Id @GeneratedValue
    val id: Long = 0,

    @OneToOne(cascade = [CascadeType.ALL])
    @JoinColumn(name = "profile_id")
    val profile: Profile
)

@Entity
data class Profile(
    @Id @GeneratedValue
    val id: Long = 0,
    val bio: String
)

2. @OneToMany

하나의 엔티티가 여러 엔티티를 참조하는 관계입니다.

@Entity
data class User(
    @Id @GeneratedValue
    val id: Long = 0,

    @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL])
    val posts: List<Post> = mutableListOf()
)

@Entity
data class Post(
    @Id @GeneratedValue
    val id: Long = 0,

    @ManyToOne
    @JoinColumn(name = "user_id")
    val user: User
)

3. @ManyToOne

여러 엔티티가 하나의 엔티티를 참조하는 관계입니다.

@Entity
data class Post(
    @Id @GeneratedValue
    val id: Long = 0,
    val content: String,

    @ManyToOne
    @JoinColumn(name = "author_id")
    val author: User
)

4. @ManyToMany

여러 엔티티가 서로 여러 개를 참조하는 관계입니다.

@Entity
data class Student(
    @Id @GeneratedValue
    val id: Long = 0,
    val name: String,

    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = [JoinColumn(name = "student_id")],
        inverseJoinColumns = [JoinColumn(name = "course_id")]
    )
    val courses: MutableList<Course> = mutableListOf()
)

@Entity
data class Course(
    @Id @GeneratedValue
    val id: Long = 0,
    val title: String
)

Fetch 전략 (EAGER vs LAZY)

EAGER

연관된 데이터를 즉시 조회하는 방식입니다.
보통 @ManyToOne, @OneToOne에서 기본값으로 사용됩니다.

@ManyToOne(fetch = FetchType.EAGER)

LAZY

연관된 데이터를 실제로 사용할 때 조회하는 방식입니다.
보통 @OneToMany, @ManyToMany에서 기본값으로 사용됩니다.

@OneToMany(fetch = FetchType.LAZY)

Cascade

부모 엔티티의 작업을 자식 엔티티에도 함께 전이하는 옵션입니다.

@OneToMany(cascade = [CascadeType.ALL])
val posts: List<Post> = mutableListOf()
  • ALL : 모두 전이
  • PERSIST : 저장 전이
  • REMOVE : 삭제 전이
  • MERGE : 병합 전이

📌 결론

Annotation은 프로그램에 추가적인 정보를 제공하는 메타데이터입니다.

스프링에서는 이 메타데이터를 바탕으로
빈 등록, 의존성 주입, 요청 매핑, 예외 처리, 트랜잭션 관리 같은 기능을 더 간편하게 구현할 수 있습니다.

즉, 특정 클래스나 메서드에 어노테이션을 붙이면
스프링이 그 의미를 해석해 알맞은 기능을 적용하게 됩니다.

기본 개념처럼 보이지만, 실제로는 스프링 개발 전반에서 계속 등장하므로
종류와 역할을 미리 잘 정리해두면 이후 학습에도 큰 도움이 됩니다.

참고 자료

  • Spring 공식 문서
  • Jakarta Persistence 공식 문서
profile
달을 향해 쏴라, 빗나가도 별이 될 테니 👊

0개의 댓글