@Component, @Service, @Repository, @Controller의 차이를 설명해주세요.
힌트
모두 @Component의 특수화이지만 계층별 역할과 부가 기능이 다릅니다.
정답 및 해설
@Component, @Service, @Repository, @Controller의 차이를 설명해주세요.
네 애노테이션 모두 Spring 컴포넌트 스캔의 대상이 되는 @Component의 특수화(specialization)입니다. 기술적으로는 모두 @Component를 메타 애노테이션으로 가지므로 Spring 컨테이너에 빈으로 등록된다는 점은 동일합니다. 하지만 각 계층의 역할과 목적을 명확히 표현하며, 일부는 계층별 부가 기능을 제공합니다.
애노테이션 계층 구조
@Component (범용 빈 등록)
├── @Service (비즈니스 로직 계층)
├── @Repository (데이터 접근 계층)
└── @Controller (프레젠테이션 계층)
└── @RestController (@Controller + @ResponseBody)
모든 특수화 애노테이션은 내부적으로 @Component를 포함합니다.
// @Service의 실제 선언
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // ← @Component를 메타 애노테이션으로 포함
public @interface Service {
@AliasFor(annotation = Component.class)
String value() default "";
}
@Component
범용적인 Spring 빈 등록 애노테이션입니다. 어느 특정 계층에도 속하지 않는 일반 컴포넌트에 사용합니다.
// 범용 유틸리티 빈
@Component
public class JwtTokenProvider {
private final String secretKey;
private final long expirationMs;
public JwtTokenProvider(
@Value("${jwt.secret}") String secretKey,
@Value("${jwt.expiration:3600000}") long expirationMs) {
this.secretKey = secretKey;
this.expirationMs = expirationMs;
}
public String generateToken(String username) {
// JWT 토큰 생성
return Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + expirationMs))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public boolean validateToken(String token) { /* ... */ }
}
// 설정 관련 컴포넌트
@Component
public class AppProperties {
@Value("${app.name:MyApp}")
private String appName;
@Value("${app.version:1.0.0}")
private String version;
// getter들
}
@Service
비즈니스 로직을 담당하는 서비스 계층에 사용합니다. 기능적으로는 @Component와 동일하지만 계층의 의도를 명확히 합니다. 트랜잭션 경계가 되는 경우가 많습니다.
@Service
@Transactional(readOnly = true) // 클래스 레벨 기본 트랜잭션
public class OrderService {
private final OrderRepository orderRepository;
private final ProductService productService;
private final PaymentService paymentService;
private final ApplicationEventPublisher eventPublisher;
public OrderService(OrderRepository orderRepository,
ProductService productService,
PaymentService paymentService,
ApplicationEventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.productService = productService;
this.paymentService = paymentService;
this.eventPublisher = eventPublisher;
}
@Transactional // 쓰기 작업에만 readOnly = false 오버라이드
public Order placeOrder(OrderRequest request) {
// 1. 재고 확인
productService.validateStock(request.getProductId(), request.getQuantity());
// 2. 주문 생성
Order order = Order.from(request);
Order saved = orderRepository.save(order);
// 3. 결제 처리
paymentService.process(saved);
// 4. 이벤트 발행
eventPublisher.publishEvent(new OrderPlacedEvent(saved));
return saved;
}
public List<Order> getUserOrders(Long userId) {
return orderRepository.findByUserId(userId);
}
}
@Repository
데이터 접근 계층(DAO)에 사용합니다. @Component와 달리 중요한 부가 기능이 있습니다.
핵심 기능: 예외 변환 (Exception Translation)
@Repository가 붙은 빈은 PersistenceExceptionTranslationPostProcessor에 의해 데이터 접근 기술별 예외(JDBC의 SQLException, JPA의 PersistenceException 등)를 Spring의 통합 예외 계층인 DataAccessException으로 자동 변환합니다.
// 예외 변환이 필요한 이유
// JDBC 사용 시: java.sql.SQLException (checked)
// JPA 사용 시: javax.persistence.PersistenceException (unchecked)
// MyBatis 사용 시: org.apache.ibatis.exceptions.PersistenceException
// → 기술마다 다른 예외 → 서비스 계층이 특정 기술에 종속됨
// @Repository가 해결:
// 모든 DB 예외 → DataAccessException 하위 예외로 통일
@Repository
public class UserRepositoryImpl implements UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepositoryImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public User findById(Long id) {
try {
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
new UserRowMapper(), id
);
} catch (EmptyResultDataAccessException e) {
return null; // DataAccessException의 하위 클래스
}
// SQLException은 Spring이 DataAccessException으로 자동 변환
}
}
Spring Data JPA에서의 @Repository
// Spring Data JPA: @Repository 없이도 동작하지만 관례상 명시하기도 함
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByAgeGreaterThan(int age);
@Query("SELECT u FROM User u WHERE u.status = :status")
List<User> findByStatus(@Param("status") UserStatus status);
}
// JPA 직접 구현
@Repository
public class ProductRepositoryCustomImpl implements ProductRepositoryCustom {
@PersistenceContext
private EntityManager em;
public List<Product> searchByCondition(ProductSearchCondition condition) {
return em.createQuery(
"SELECT p FROM Product p WHERE p.price BETWEEN :min AND :max",
Product.class)
.setParameter("min", condition.getMinPrice())
.setParameter("max", condition.getMaxPrice())
.getResultList();
}
}
@Controller
Spring MVC의 프레젠테이션 계층에 사용합니다. DispatcherServlet이 요청을 라우팅할 때 @Controller 빈을 스캔합니다.
@Controller
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
// View 이름을 반환 (Thymeleaf, JSP 등 템플릿 엔진 사용)
@GetMapping("/{id}")
public String getUser(@PathVariable Long id, Model model) {
User user = userService.findById(id);
model.addAttribute("user", user);
return "user/detail"; // templates/user/detail.html
}
// @ResponseBody와 함께 JSON 반환
@GetMapping("/{id}/json")
@ResponseBody
public UserDto getUserJson(@PathVariable Long id) {
return UserDto.from(userService.findById(id));
}
@PostMapping
public String createUser(@ModelAttribute UserCreateForm form,
BindingResult result,
RedirectAttributes attrs) {
if (result.hasErrors()) {
return "user/create";
}
userService.create(form);
attrs.addFlashAttribute("message", "회원가입 완료");
return "redirect:/users";
}
}
@RestController
@Controller + @ResponseBody의 조합으로, REST API 엔드포인트 구현에 특화되어 있습니다.
// @RestController = @Controller + @ResponseBody
@RestController
@RequestMapping("/api/v1/products")
public class ProductApiController {
private final ProductService productService;
public ProductApiController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public ResponseEntity<List<ProductDto>> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
List<ProductDto> products = productService.findAll(page, size);
return ResponseEntity.ok(products);
}
@GetMapping("/{id}")
public ResponseEntity<ProductDto> getProduct(@PathVariable Long id) {
return productService.findById(id)
.map(ProductDto::from)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<ProductDto> createProduct(
@RequestBody @Valid ProductCreateRequest request) {
ProductDto created = productService.create(request);
URI location = URI.create("/api/v1/products/" + created.getId());
return ResponseEntity.created(location).body(created);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.delete(id);
return ResponseEntity.noContent().build();
}
}
계층형 아키텍처와 애노테이션 배치
HTTP 요청
↓
@Controller / @RestController (프레젠테이션 계층)
↓ 호출
@Service (비즈니스 로직 계층)
↓ 호출
@Repository (데이터 접근 계층)
↓ 접근
Database
// 계층별 역할 분리 예시
// 1. Controller: HTTP 요청/응답 처리, 입력 검증
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<OrderDto> createOrder(
@RequestBody @Valid OrderCreateRequest request,
@AuthenticationPrincipal UserPrincipal principal) {
OrderDto result = orderService.placeOrder(principal.getId(), request);
return ResponseEntity.status(HttpStatus.CREATED).body(result);
}
}
// 2. Service: 비즈니스 규칙, 트랜잭션 경계
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
public OrderDto placeOrder(Long userId, OrderCreateRequest request) {
inventoryService.reserve(request.getItems()); // 재고 예약
Order order = Order.create(userId, request);
return OrderDto.from(orderRepository.save(order));
}
}
// 3. Repository: 데이터 CRUD
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByUserIdOrderByCreatedAtDesc(Long userId);
}
네 애노테이션 비교 요약
| 구분 | @Component | @Service | @Repository | @Controller |
|---|---|---|---|---|
| 계층 | 범용 | 비즈니스 로직 | 데이터 접근 | 프레젠테이션 |
| 빈 등록 | O | O | O | O |
| 컴포넌트 스캔 | O | O | O | O |
| 예외 변환 | X | X | O (DataAccessException) | X |
| 요청 매핑 | X | X | X | O (DispatcherServlet) |
| View 반환 | X | X | X | O (템플릿 엔진) |
| 트랜잭션 경계 | - | 주로 여기 | - | - |
| 사용 예 | 유틸, 헬퍼 클래스 | 비즈니스 서비스 | DAO, JPA Repository | MVC Controller |
| @RestController | - | - | - | = @Controller + @ResponseBody |