
백엔드 개발자를 준비하면서 생긴 궁금증을 정리한 포스트입니다.
Annotation은 사전적으로는 주석이라는 뜻이지만,
자바와 스프링에서의Annotation(@)은 코드에 특별한 의미를 부여하는 문법입니다.
스프링 프레임워크에서 어노테이션은 애플리케이션 구성, 빈 등록, 의존성 주입, 요청 매핑 등을 더 간편하게 처리할 수 있도록 도와줍니다.
즉, 프로그램 자체의 실행 로직이라기보다 프로그램에 대한 추가 정보(메타데이터) 를 제공하는 방식이라고 볼 수 있습니다.
어노테이션을 사용하면 설정을 코드에 명확하게 표현할 수 있어 가독성이 좋아지고, 반복되는 설정을 줄이는 데에도 도움이 됩니다.
어노테이션은 보통 다음과 같은 방식으로 사용합니다.
@ 기호를 사용해 선언합니다.즉, 특정 클래스나 메서드에 어노테이션을 붙이면
스프링이 그 의미를 해석해 알맞은 기능을 수행하게 됩니다.
@SpringBootApplication@SpringBootApplication은 스프링 부트 애플리케이션의 시작점을 나타내는 대표적인 어노테이션입니다.
보통 메인 클래스에 붙으며,
스프링 부트 애플리케이션을 실행할 때 가장 먼저 보게 되는 어노테이션이기도 합니다.
이 어노테이션 하나로 다음과 같은 기능을 함께 사용할 수 있습니다.

즉, 스프링 부트 애플리케이션을 실행하기 위한 핵심 어노테이션이라고 볼 수 있습니다.
@Component@Component는 스프링 빈으로 등록할 클래스를 지정할 때 사용합니다.
@Component
class MyService {
fun getMessage() = "Hello, Spring Boot!"
}
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를 생략하고 생성자 주입을 사용하는 방식도 많이 사용합니다.
@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"
}
@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@RequestPart는 multipart/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
@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)
}
}
@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}"
JPA는Java Persistence API의 약자로,
자바 객체와 관계형 데이터베이스를 매핑하기 위한 표준 인터페이스입니다.
객체와 데이터베이스 테이블을 매핑하는 기술을 의미합니다.
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@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
)
@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
)
@ManyToOne여러 엔티티가 하나의 엔티티를 참조하는 관계입니다.
@Entity
data class Post(
@Id @GeneratedValue
val id: Long = 0,
val content: String,
@ManyToOne
@JoinColumn(name = "author_id")
val author: User
)
@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
)
연관된 데이터를 즉시 조회하는 방식입니다.
보통 @ManyToOne, @OneToOne에서 기본값으로 사용됩니다.
@ManyToOne(fetch = FetchType.EAGER)
연관된 데이터를 실제로 사용할 때 조회하는 방식입니다.
보통 @OneToMany, @ManyToMany에서 기본값으로 사용됩니다.
@OneToMany(fetch = FetchType.LAZY)
부모 엔티티의 작업을 자식 엔티티에도 함께 전이하는 옵션입니다.
@OneToMany(cascade = [CascadeType.ALL])
val posts: List<Post> = mutableListOf()
ALL : 모두 전이PERSIST : 저장 전이REMOVE : 삭제 전이MERGE : 병합 전이
Annotation은 프로그램에 추가적인 정보를 제공하는 메타데이터입니다.
스프링에서는 이 메타데이터를 바탕으로
빈 등록, 의존성 주입, 요청 매핑, 예외 처리, 트랜잭션 관리 같은 기능을 더 간편하게 구현할 수 있습니다.
즉, 특정 클래스나 메서드에 어노테이션을 붙이면
스프링이 그 의미를 해석해 알맞은 기능을 적용하게 됩니다.
기본 개념처럼 보이지만, 실제로는 스프링 개발 전반에서 계속 등장하므로
종류와 역할을 미리 잘 정리해두면 이후 학습에도 큰 도움이 됩니다.