Spring을 Kotlin으로 쓰는 공부를 하다가 Spring의 근본적인 이해가 부족한 것 같아 공부하는 내용을 포스팅한다. 해당 내용은 객체지향 프로그래밍의 DI(의존성 주입)에 대한 이해를 선행한다.
Spring은 기본적으로 OOP(객체지향 프로그래밍)의 패러다임을 따르고 있다. 따라서 클래스 단위로 코드를 작성하며, 이 클래스에 대한 인스턴스를 관리하여 작동하는 방식이다. 그래서 Spring은 인스턴스에 대한 효율적인 관리가 특히 중요하다.
싱글톤 패턴이란 객체지향 프로그래밍의 디자인 패턴 중 "생성 패턴"의 하나로, 필요에 의해 클래스의 인스턴스를 하나만 생성하여 인스턴스를 공유하는 방식이다.
Spring은 클래스에서 다른 클래스의 인스턴스를 참조하여 메소드를 사용해야하는 상황이 빈번하다. 예를 들어 Controller 단에서 Service 클래스의 메소드를 호출해야 하는 경우에 요청에 대해 모든 Service 인스턴스를 생성해야 한다면, 해당 인스턴스는 쓸데없는 메모리 차지를 할 것이다.
그렇기 때문에 Spring은 Service의 인스턴스를 싱글톤 패턴을 적용하여 리소스를 효율적으로 관리한다.
그렇다면 Spring에서는 어떤 방법으로 싱글톤 패턴을 적용하고 있을까? 이를 알기 위해선 IoC를 알 필요가 있다.
IoC는 인스턴스의 생성과 의존성 관리를 역전한다는 뜻이다.
기본적으로 인스턴스의 생성과 관리는 개발자가 직접 관리하는데, 이를 모두 관리하고 프로그래밍하기에는 신경을 쓸 부분이 한 두가지가 아니다.
Spring 프레임워크는 이를 프레임워크가 관리하여 개발자의 편의성을 증대시킨다. 개발자가 @Bean
또는 @ComponentScan
으로 컴포넌트를 등록해주면 자체적으로 IoC Container 내에서 인스턴스의 생성 및 관리를 해준다.
등록하면 IoC Container는 자체적으로 인스턴스를 관리하는데, 해당 인스턴스는 싱글톤 패턴으로 관리한다.
IoC Container는 프로그램 시작 시에 미리 빈에 등록된 인스턴스들을 생성해놓는다. 이후에 예를 들어 @Autowired
와 같이 Controller에서 Service의 메소드를 사용하기 위해 인스턴스를 참조해야 할 때, IoC Container 내에 미리 생성해 둔 인스턴스를 사용한다.
때문에 DI로 인스턴스를 필요로 할 때마다 하나의 인스턴스를 참조하여 관리하도록 한다.
여기서 불필요한 메모리 사용, 객체 참조에 대한 순환 참조 문제 등의 문제가 생기는데, 이는 Lazy Initialization(지연 초기화)을 사용하여 해결한다. 이 부분에 대해서는 다음에 알아보도록 하겠다.
@Bean
또는 @ComponentScan
을 통해 공유할 클래스 또는 인터페이스를 등록한다.@Autowired
를 사용하여 해당 인스턴스를 불러와 참조한다.chatGPT >>
SOLID 원칙 중 하나로, 아래의 철학을 담고 있다.
고수준 모듈은 저수준 모듈에 의존해서는 안 된다.
둘 다 추상화(abstraction)에 의존해야 한다.
추상화는 구체화에 의존해서는 안 된다. 구체화가 추상화에 의존해야 한다.
DIP는 클래스에 직접 접근하지 않고, 인터페이스에 접근하여 다루는 것을 말한다.
기존에 개발하던 방식은 소규모 프로젝트였기 때문에 이 방법을 이해하는 데에는 무리가 있었다. 왜냐하면 각각의 클래스를 생성하는 점이 인터페이스를 따로 추가로 생성하는 것보다 코드 길이가 짧고 사용할때도 간단하기 때문이다.
그러나 규모가 큰 서비스나 테스트 환경에서는 이 원칙이 중요하다고 한다. 예시 코드를 보면서 설명하겠다.
public interface PaymentService {
String pay(int amount);
}
@Service
@Primary // 기본 주입 대상 (선택 사항)
public class KakaoService implements PaymentService {
@Override
public String pay(int amount) {
return "카카오페이로 " + amount + "원 결제 완료";
}
}
@Service
public class TossService implements PaymentService {
@Override
public String pay(int amount) {
return "토스페이로 " + amount + "원 결제 완료";
}
}
이렇게 인터페이스를 통해 접근하면 각각의 구현체에 직접적으로 접근하지 않음으로써 확장성과 유지보수성이 증가한다.
또한, IoC에 의해 관리되는 인스턴스에서 벗어나는 구현체를 생성할 수 있기 때문에 가짜 인스턴스를 생성할 구현체를 추가할 수 있다. 이로 인해 테스트 환경에서 가짜 인스턴스를 사용해서 테스트가 가능하다.