
이전 포스팅에서 스프링이 뭔지에 대해 간단하게 알아보았습니다.
그래서 이번에는 스프링의 핵심 아키텍쳐인 스프링 컨테이너에 대해 알아보고자 합니다.
그전에 몇가지 집고 넘어가야 할 개념들이 있어 빠르게 한번 알아보겠습니다.
제어의 역전(Inverse of Control, IoC)는 사용할 객체를 직접 생성하지 않고, 객체의 생명주기 관리를 외부(스프링 컨테이너)에 위임하는 설계 방식입니다.
IoC는 프레임워크를 설계하는 방식 중 하나입니다. IoC을 사용하게 되면 객체의 생명주기를 개발자가 아닌 외부에서 관리하기 때문에 개발자는 비지니스 로직에 더욱 집중할 수 있다는 장점이 있습니다.
그렇다면 개발자가 작성한 클래스로 만들어진 객체를 스프링 프레임워크가 관리할 수 있도록 해야되는데 어떻게 그게 가능할까요?
스프링에서는 DI(Dependency Injection)라는 개념을 사용하여 해결합니다.
의존성 주입(DI, Dependency Injection)은 제어의 역전을 구현하는 방법 중의 하나로, 외부에서 두 객체 간의 관계를 결정해주는 디자인 패턴 입니다.
스프링에서는 특정 객체가 사용할 또 다른 객체를 직접 생성하지 않고 외부 컨테이너가 생성한 객체를 주입받아 사용합니다. DI를 사용하지 않은 코드와 사용한 코드를 서로 비교하는 예제를 통해 DI가 뭔지 그리고 왜 사용해야 하는지 자세히 알아보겠습니다.
알림을 보낼 때 메시지 서비스에서 제공하는 전송 메시지 문구를 사용하여 알림을 보내는 기능을 구현한다고 가정해보겠습니다.
// 메일 발송 서비스 클래스
class EmailService {
public void sendEmail(String message) {
System.out.println("Sending email: " + message);
}
}
// 알림 서비스 클래스
class NotificationService {
private EmailService emailService = new EmailService(); // 직접 객체를 생성
public void sendNotification(String message) {
emailService.sendEmail(message);
}
}
// 사용 예시
public class Main {
public static void main(String[] args) {
NotificationService notificationService = new NotificationService();
notificationService.sendNotification("Hello, DI without!");
}
}
위의 코드는 의존성 주입 없이 NotificationService 객체에 EmailService 객체를 직접 넣은 예시 코드입니다. 해당 코드의 단점은 무엇일까요?
1. 객체가 서로 강한 결합을 가지고 있다.
NotificationService는 EmailService 객체를 직성 생성하였기 때문에 가장 의존 관계를 가지고 있습니다. 그렇기 때문에 EmailService의 구현이 변경되면 NotificationService도 수정해야 합니다.EmailService 의 sendEmail 메소드만 사용하고 있어 복잡하지 않지만 코드가 커지고 EmailService 에서 다양한 메소드를 사용할 경우 수정해야할 코드는 기하급수적으로 늘어날 것입니다.2. 객체들 간의 관계가 아니라 클래스들 간의 관계가 맺어져 있다.
NotificationService와 EmailService는 객체들 간의 관계가 아니라 클래스들 간의 관계가 맺어져 있다는 문제가 있습니다.위의 코드를 의존성 주입 방식을 활용하여 아래 코드와 같이 개선해보았습니다.
// 메일 발송 서비스 인터페이스
interface MessageService {
void sendMessage(String message);
}
// 메일 발송 서비스 구현 클래스
class EmailService implements MessageService {
public void sendMessage(String message) {
System.out.println("Sending email: " + message);
}
}
// 알림 서비스 클래스
class NotificationService {
private MessageService messageService;
// 의존성 주입: 외부에서 객체를 주입받음
public NotificationService(MessageService messageService) {
this.messageService = messageService;
}
public void sendNotification(String message) {
messageService.sendMessage(message);
}
}
// 사용 예시
public class Main {
public static void main(String[] args) {
MessageService emailService = new EmailService();
NotificationService notificationService = new NotificationService(emailService); // 의존성 주입
notificationService.sendNotification("Hello, DI with!");
}
}
MessageService 라는 인터페이스를 하나 추가했고 NotificationService 클래스에는 이전과 달리 객체를 직접 생성하는 것이 아닌 messageService 인터페이스를 매개변수로 받고 있습니다. 이후 main() 함수에 messageService의 구현체(EmailService)를 생성하고 주입받고 있습니다.
인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고 런타임 시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있게 해줍니다.
스프링에서도 특정 객체가 사용할 또 다른 객체를 직접 생성하지 않고 외부 컨테이너가 생성한 객체를 주입받아 사용합니다.
스프링에서는 다양한 의존성 주입 방식을 지원합니다.
@Service
public class UserService {
private UserRepository userRepository;
private MemberService memberService;
@Autowired // 생략 가능
public UserService(UserRepository userRepository, MemberService memberService) {
this.userRepository = userRepository;
this.memberService = memberService;
}
}
@Service
public class UserService {
private UserRepository userRepository;
private MemberService memberService;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private MemberService memberService;
}
스프링 컨테이너는 스프링 프레임워크의 핵심 컴포넌트입니다.
스프링 컨테이너는 주로 객체의 생명주기를 관리하고, 생성된 객체들의 추가적인 기능들을 제공합니다.
앞서 설명한 IoC의 실체라고 볼 수 있습니다.
스프링에서는 자바 객체를 빈(Bean)이라고 부릅니다.

일반적으로 Spring Container는 ApplicationContext를 의미합니다.
ApplicationContext는 BeanFactory를 비롯한 여러가지 인터페이스를 다중 상속한 인터페이스 입닌다.
상속을 받은 각각의 부모 인터페이스들의 역할은 다음과 같습니다.
BeanFactory : Bean을 관리하고 검색하는 기능을 제공하는 인터페이스MessageSource : 메세지 다국화를 위한 인터페이스EnvironmentCapable : 개발, 운영 등 환경을 분리해서 처리하고, 애플리케이션 구동에 필요한 정보들을 관리하기 위한 인터페이스ApplicationEventPublisher : 이벤트를 발행하고 구독하는 모델을 편리하게 지원하는 인터페이스ResourceLoader : 파일, class path 등 리소스를 읽어오기 위한 인터페이스 Spring 공식문서 상 컨테이너를 사용해야 할때 특별한 이유가 없다면, ApplicationContext 를 사용하라고 권장합니다. 그 이유는 BeanFactory를 포함한 여러 인터페이스의 모든 기능을 ApplicationContext가 포함하고 있기 때문입니다.
https://hudi.blog/inversion-of-control/
https://mangkyu.tistory.com/125
https://lucas-owner.tistory.com/39
https://innovation123.tistory.com/167
https://junhkang.tistory.com/43