백엔드/SpringBoot

[SpringBoot] 스프링에서의 Service 와 Bean의 차이점

연유뿌린빙수 2025. 10. 4. 03:18

스프링의 강점 중 가장 대표적인 것은,
개발자가 모든 객체 생성을 일일이 관리하지 않아도 된다는 점이다.

 

스프링 컨테이너가 애플리케이션을 실행하면서 필요한 객체들을 알아서 생성해주고,
서로 필요한 의존성을 연결해준다!

 

그렇다면 스프링이 객체를 인식하도록 만들어려면 어떻게하는 것인가?


 

대표적으로는 다음과 같은 두 가지 방식이 존재한다.

  1. 명시적으로 등록하는 방법: @Configuration + @Bean
  2. 자동으로 등록하는 방법: @Component, @Service, @Repository, @Controller

 

 

개발을 하다보면,
1번의 기능도 자주 사용하고, 2번도 굉장히 편리하게 사용하지만,
생각보다 별 의식 없이 기계적 학습으로만 사용하는 경우가 많다.

 

본격적으로 두 방식을 비교하기 이전에(사용해보기 이전에),
먼저 스프링의 핵심 개념인 의존성(Dependency)  의존성 주입(Dependency Injection) 에 대하여 알아보자



Dependency

의존성이란, 말 그대로 한 클래스가 다른 클래스에 의존을 하고 있다는 것이다!

"한 클래스가 다른 클래스의 기능을 필요로 하는 관계"를 의미한다.

 

어떤 클래스에서 다른 클래스를 호출하여 그대로 사용하고 싶다면,
의존성 주입이 필요하다.

 

가령 OrderService가 주문을 생성하려면 PaymentService (결제 서비스)가 필요하다.
이 경우에는 OrderService는 PaymentService에 의존한다!

 

순수 POJO 식 접근으로 해보자.

 

  • PaymentService
public class PaymentService {
    public void pay() {
        System.out.println("결제 완료!");
    }
}
  • OrderService
public class OrderService {
    private PaymentService paymentService = new PaymentService();

    public void createOrder() {
        paymentService.pay();
        System.out.println("주문 생성 완료!");
    }
}

 

여기서 OrderService가 "직접" PaymentService를 생성(new) 하고 사용한다.

 

 

이렇게 하면 문제가 생긴다.

 

만약 결제 방식이 바뀐다면?? -> OrderService코드를 수정해야한다.
그리고 테스트를 할 때에는 new 생성자로 인하여 Mock으로 대체해주는 것이 어렵다.

 

즉, 클래스 간의 결합도가 높아지고 확장성이 떨어지는 방법이다.

 

Dependency Injection(의존성 주입)

 

그래서 스프링은 이러한 문제를 해결해주기 위한 기술이 존재한다!

 

이것이 스프링의 3대 기술 중 하나인 의존성 주입이다.

의존성을 외부에서 주입(Injection) 해준다.

 

final을 통한(AutoWired 생략) 생성자 주입으로 예시를 봐보자.

@Service
@RequiredArgsConstructor
public class OrderService {
    private final PaymentService paymentService;

    public void createOrder() {
        paymentService.pay();
        System.out.println("주문 생성 완료!");
    }
}

 

위의 final을 통한 생성자 주입 방식으로 의존성을 주입하였다.

 

이로써 OrderService는 PaymentService를 직접 만들지 않고, 단지 "필요하다" 라고만 선언한다. 그렇게함으로써 스프링이 자동으로 PaymentService 객체를 찾아서 OrderService에 넣어주는 것이다!!

 

이러면 서비스에서 테스트도 실제 결제 API를 사용하지 않고 Mock 객체를 주입해주면 된다.

public class MockPaymentService extends PaymentService {
    @Override
    public void pay() {
        System.out.println("테스트 결제 처리");
    }
}

@Test
void testOrder() {
    OrderService orderService = new OrderService(new MockPaymentService());
    orderService.createOrder();
}

 

위 처럼 가짜 Mock 객체를 직접 주입해주면서 코드를 작성해주면 되기 때문에

 

실제 결제 없이도 테스트가 가능하다.

의존성 주입으로 컴포넌트로 스캔된 클래스의 의존성이 무엇인지 확인하고,
해당 의존성을 해결시켜주는 것이다.

 

즉, 다시 한 번 서술하지만

 

개발자가 직접 new 하지 않아도 알아서 스프링이 객체를 대신 관리해주고 주입해주는 것이 DI이다.

  • 예전 방식(스프링 이전): 개발자가 직접 객체를 생성하고 주입해야 함
  • 스프링 방식: 컨테이너가 필요한 의존성을 찾아서 주입해줌

이런 제어권의 역전을 IoC(Inversion of Control) 이라고 부른다.

 
 



이제 주입에 대한 어노테이션을 통한 방법을 알아보자.

@Configuration + @Bean

이는 명시적으로 등록하는 방법으로,
자바 설정 파일에서 직접 객체를 생성하고 스프링 컨테이너에 등록하는 방식이다.

 

@Configuration 클래스 안에서 @Bean 메서드를 만들어서 객체를 반환하면,
그 반환 객체가 스프링 컨테이너 빈으로 등록된다.

@Configuration
public class AppConfig {
	
    @Bean
    public UserService userService() {
    	return new UserService();
    }
} 

 

이 방식의 장점은 위에서처럼 객체 생성 과정을 직접 제어하는 것이 가능하다.

 

예를 들어서 파라미터를 넘기는 경우, 다른 빈을 조합해서 새로운 빈을 만드는 경우, 팩토리 패턴이나 조건부를 생성하는 등 복잡한 로직이 필요한 경우에 사용한다.

 

하지만 코드가 장황해지고,
이것이 단순히 빈 등록인지 서비스 로직인지에 대한 구분이 모호해질 수 있다.

 

그래서 일반적인 비즈니스 로직에서는 굳이 사용할 필요가 적고,
주로 외부 라이브러리 객체를 스프링 빈으로 등록할 때 활용된다.
(서비스 어노테이션을 통한 등록만으로도 충분히 구현이 가능하기에 굳이 싶다.)

 

@Service(@Component), @Repository, @Controller

 

이는 어노테이션을 통한 자동 등록 방식( 컴포넌트 스캔 기반)으로,
클래스 위에 어노테 이션 하나만 붙이면 스프링이 알아서 자동으로 빈으로 등록해준다!

 

@Service
public class UserService {
    public String hello() {
        return "hello";
    }
}

 

위와 동일한 기능인데도 코드가 깔끔하고 간단해진다.


서비스 역할임을 명시적으로 표현이 가능해서 가독성이 높아지고 레이어 구분이 가능하다.

계층 구조를 구분해줄 수 있다는 것

또한 스프링이 알아서 빈을 관리해주기 때문에 생산성이 높다.

 

등록방식이 고정적이고 생성자나 파라미터나 등록 로직을 세밀하게 제어하기 어렵다는 점이 존재하긴 하는데,

대부분의 비즈니스 로직에서 권장되는 방식이다.

복잡한 경우가 아니라면 @Service 같은 어노테이션을 붙이는 것만으로도 충분히 구현이 가능하다.



 

결론적으로는 둘 다 결국 스프링 컨테이너에 빈을 등록하는 점이다.

일반적인 애플리케이션 개발에서는 @Component 기반 자동 등록을 사용하고, 특수한 경우에만 @Bean을 사용하면 된다는 것만 알아두자