스프링

스프링 추상화와 의존성 주입

yoon4360 2025. 3. 31. 22:15

 

스프링을 공부하다 보면 인터페이스를 왜 쓰는지, @Service와 @Repository는 뭐가 다른지,
스프링 컨텍스트는 왜 필요한지 궁금해지기 마련이다.

이번 글에서는 스프링의 추상화, 의존성 주입(DI), 컴포넌트 스캔까지 한 번에 정리해서
스프링의 기본 구조를 깔끔하게 이해해보자.

 


추상화란?

추상화란 어떤 기능을 “무엇을 한다” 수준까지만 정의하고,
“어떻게 한다”는 각기 다른 방식으로 구현할 수 있게 하는 것이다.

즉, 인터페이스(Interface) 를 정의하고
그 인터페이스를 구현하는 여러 클래스(구현체)를 만들 수 있다.

 

이런 추상화가 왜 중요할까?

// 인터페이스
public interface NotificationSender {
    void send(String message);
}

// 구현 1
public class EmailSender implements NotificationSender {
    public void send(String message) {
        System.out.println("메일 전송: " + message);
    }
}

// 구현 2
public class SmsSender implements NotificationSender {
    public void send(String message) {
        System.out.println("SMS 전송: " + message);
    }
}

 

 

NotificationSender를 사용하는 서비스는 이 구현이 Email인지 SMS인지 알 필요가 없다.

나중에 KakaoTalkSender를 추가해도 기존 코드는 변경할 필요 없다.

따라서 인터페이스를 사용하면 변경에 유연하고, 확장에 강하다.

 

스프링에서 인터페이스 + DI 사용하기

스프링은  보통 다음 구조를 따른다.

Main (앱 실행) 
   ↓
Service 클래스 (@Service)
   ↓
인터페이스 (기능 정의)
   ↓
구현 클래스 (@Repository 등)

 

스프링이 관리하는 객체를 스프링 컨텍스트에 등록하면,

개발자가 new로 객체를 만들지 않아도 스프링이 필요한 구현체를 알아서 주입(DI)해준다.

 


 

의존성 주입과 컴포넌트 스캔

스프링은 @Component가 붙은 클래스를 자동으로 찾아서 빈으로 등록하고,
필요한 곳에 @Autowired 또는 생성자 방식으로 주입해준다.

 

주요 컴포넌트 애너테이션

@Component 모든 스프링 빈의 기본형
@Service 서비스 계층에 책임 있는 클래스에 사용 (비즈니스 로직)
@Repository DB에 접근하는 클래스에 사용 (DAO/레포지터리)
@Controller 웹 요청을 받는 클래스에 사용

 

이 애너테이션들은 모두 @Component의 특수화 형태이며, 스프링이 역할에 따라 자동 처리할 수 있도록 도와준다.

 

 

Notificaiton 서비스 예시

public interface NotificationSender {
    void send(String message);
}
@Service
public class NotificationService {

    private final NotificationSender sender;

    public NotificationService(@Qualifier("smsSender") NotificationSender sender) {
        this.sender = sender;
    }

    public void notifyUser(String msg) {
        sender.send(msg);
    }
}

 

@Repository("emailSender")
public class EmailSender implements NotificationSender {
    public void send(String message) {
        System.out.println("📧 이메일 전송: " + message);
    }
}

@Repository("smsSender")
public class SmsSender implements NotificationSender {
    public void send(String message) {
        System.out.println("📱 SMS 전송: " + message);
    }
}

 

인터페이스는 추상적인 계약일 뿐이고, 직접 기능을 수행하지 않으므로 스프링 컨텍스트에 굳이 등록할 필요는 없다.

반면, 실제 로직을 수행하는 구현 클래스는 컨텍스트에 등록해야 한다.

 


 

마무리

유지보수 가능한 애플리케이션을 위해 인터페이스로 역할을 분리하고,

스프링 컨텍스트에 필요한 구현 객체만 등록해서 스프링이 알아서 주입하도록 위임하자.