DI에 관한 글을 적어놓고 다음 포스트에서 더 이야기한다고 했지만, 이것저것 하느라 바빠서 시간이 별로 없었습니다.
그래서 이제야 2장을 쓰게 되는데 사실 1장을 작성하고 2장으로 어떤 걸 작성하려 했는지 잘 기억이 나지 않아서 제가 적고 싶은 대로 적어보려고 합니다.
일단 1장을 먼저 간단하게 요약하겠습니다.
자바는 객체 지향 언어로서 5가지 주요 원칙이 있는데, 이 원칙들의 궁극적인 목표가 가독성이 좋고, 유지보수성이나 확장성이 좋은 코드를 작성하는 것입니다.
그중 단일 책임 원칙이나 인터페이스 분리 원칙을 보면, 각 객체는 하나의 책임만을 가져야 하고, 자신이 사용하지 않는 메서드는 들고 있을 필요가 없다는 것을 뜻합니다.
그래서 이것을 쉽게 말하자면 결국 객체에 쓰지도 않는 기능을 넣지 말고, 한 번에 많은 기능을 넣지 말고 여러 클래스로 분리하라는 것이죠
만약 이러한 객체 지향 원칙을 위반하게 되면 객체의 결합도가 높아지게 됩니다. 객체의 책임이 많아져서 결합도가 높아지면 당연히 이에 대한 의존 대상이 늘어날 것이고, 무언가 하나 변경해야 한다면 원칙을 잘 준수했다면 바꾸지 않아도 될 것들을 줄줄이 바꿔줘야 합니다. 그래서 유지보수나 확장에서 어려움이 생기게 되죠. 그래서 이것을 최소화하는 것이 목표이고, DI라는 개념을 사용하게 됩니다.
DI는 의존성 주입으로 각 객체간 직접 결합하는 것이 아닌 외부에서 주입하는 것입니다. 그래서 1장에서 했던 것처럼 다른 객체를 사용하려는 객체에서 new로 생성하는 것이 아닌 의존성을 주입하는 객체를 별도로 만드는 것입니다.
1장에서는 그 역할을 Main 클래스가 했고, B클래스가 바뀌었을 시 A를 바꾸는 것이 아닌 Main클래스만 바꾸어주면 된 것입니다.
이렇게 하면 클래스가 늘어나서 변경사항이 일어나도 해당 클래스와 Main 클래스만 변경해 주면 되는 것이죠.
그래서 이러한 DI를 Spring에서는 개발자가 아닌 Spring에서 직접 관리해 줍니다. 클래스를 작성하고 Bean 객체로 등록해 주면 알아서 스프링 컨테이너에서 관리해 주고 필요할 때마다 DI를 통해 사용해 주면 됩니다.
그러면 2장에서는 실제로 스프링에서 어떻게 사용하는지를 알려드리겠습니다.
대표적으로 3가지 방법이 있습니다.
1. 생성자 주입
2. setter 주입
3. 필드 주입
일단 결론부터 말씀드리자면 setter 주입과 필드 주입은 잘 사용하지 않습니다.
필드 주입은 아주 간단하지만 final을 사용하지 못해 불변성을 보장할 수 없습니다. 또한 테스트에 용이하지 않고, 필드에서 주입하게 되면 내부 구현에 묶이기 때문에 DI원칙에 위반됩니다.
setter 주입은 그래도 필드 주입보다는 사용하는 편입니다. 필드 주입과는 다르게 테스트도 가능하고 만약 해당 의존성이 필수가 아니거나 동적으로 변경해야 하는 일이 생긴다면 setter 주입을 종종 사용하기도 합니다. 그리고 이러한 장점 때문에 코드에서 의도를 드러내기가 좋습니다.
하지만 setter 주입 역시 불변성을 보장하기 어렵고, 실제로 코드르 자다 보면 의존성을 크게 바꿀일이 없습니다. 그리고 컨테이너가 관리하지 않는 환경(Test, 순수 Java 등)에서 new를 통해 생성할 때 setter를 호출하지 않으면 NPE 문제가 발생하게 됩니다.
일단 필드 주입부터 예시를 보여드리겠습니다.
@Service
public class MemberService {
@Autowired
private MemberRepository memberRepository;
}
이와 같이 의존하고자 하는 객체에 @Autowired 어노테이션을 붙여주면 됩니다.
setter 주입도 말 그대로 setter를 통해 주입하면 됩니다.
@Component
public class MemberService {
private MemberRepository memberRepository;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
마지막으로 생성자 주입입니다. 생성자 주입을 가장 많이 쓴다고 했는데 생성자 주입은 여러 가지 장점이 있습니다. 일단 불변성입니다. Java에서는 필드에서 선언한 키워드가 변경되지 않도록 하고자 할 때 final을 붙입니다. 그리고 final로 생성한 객체는 생성자에서 객체를 생성해야 합니다. 만약 생성하지 않는다면 애플리케이션이 실행되지 않습니다.
객체가 언제든 바뀔 수 있다는 것은 꽤나 큰 리스크가 존재하는데 개발자가 의도하지 않는 방향으로 흘러갈 수 있습니다. Spring Bean은 싱글톤으로 대부분 관리가 되는데 만약 여러 곳에서 객체를 사용할 때 어느 지점에서 변경이 일어나게 된다면 문제가 일어날 수 있습니다. 하지만 객체가 불변하다면 이러한 문제에서 자유로워지는 것이죠
또한, @Autowired 같은 어노테이션을 쓰지 않기 때문에 해당 객체가 Spring에 어느 정도 제약이 풀리게 됩니다.
이 말은 Spring 없어도 객체 생성과 실행이 가능하다는 것입니다.
이게 주는 장점으로는 순수 단위 테스트, 혹은 순수 자바를 써야 하는 다른 모듈 등에서 생성자만으로 객체 생성이 가능하다는 것입니다.
@Autowired를 사용하게 되면 객체를 주입할 때도 스프링 컨테이너가 관리하는 Bean 객체만이 가능합니다. 이게 통합 테스트에서는 상관없지만 단위 테스트에서는 순수 자바 객체인 Mock 객체를 주입해야 하지만, Bean 객체만이 가능하다면 테스트가 어려워지게 되죠
그래서 생성자는 그냥 순수 자바 객체도 사용이 가능하기 때문에 테스트에 용이하다는 것입니다.
@Component
@RequiredArgsConstructor
class MemberService {
private final MemberRepository memberRepository;
}
생성자 주입도 쉽습니다. setter 대신 생성자로 넣어주면 되고 @Autowired가 필요 없어집니다. 이렇게만 선언해도 스프링에서 알아서 컨테이너에서 MemberRepository를 찾아서 집어넣어 줍니다.
그러면 만약 의존하는 객체가 많아지면 일일이 생성자에 넣어줘야 하지 않나라는 의문이 드시는 분들도 계실 겁니다.
이러한 불편함을 해소하기 위해 Spring에서는 Lombok이라는 외부 라이브러리를 사용 가능합니다.
Lombok은 객체의 생성자, setter, getter 등 어노테이션만 달면 자동으로 만들어주는 라이브러리인데, @RequiresArgsConstrutor라는 어노테이션이 있습니다.
자바에서는 final을 붙이면 생성자에서 무조건 정의를 해줘야 한다고 했는데 이 어노테이션을 붙이면 final 키워드가 붙은 객체를 자동으로 포함해서 생성자를 만들어줍니다.
@Component
@RequiredArgsConstructor
class MemberService {
private final MemberRepository memberRepository;
}
위처럼 생성자를 따로 작성하지 않아도 되며, 아무리 많은 필드가 있어도 어노테이션 한 줄이면 됩니다.
지금까지 DI에 대해서 알아보았습니다.
사실 Spring을 처음 배울 때 정말 중요한 개념이지만, 이해하기가 어려운 개념이라고 생각합니다.
아무리 여러 번 봐도 이해가 안 가고 까먹게 되고 하는데 좀 쉽게 이해할 수 있도록 마지막으로 정리하자면
이러한 개념들은 결국 가독성, 유지보수성, 확장성 등을 고려하기 위해 나온 개념들입니다.
DI도 역시 마찬가지인데요, 객체 간의 결합도를 떨어트려 유지보수나 확장성, 혹은 변경에 용이하게 대처하기 위해서입니다.
여기서 Spring에서는 한술 더 떠서 객체 관리를 개발자가 아닌 Spring에게 맡겨버립니다. 이것을 IoC(제어의 역전)라고 하죠
그래서 Spring에서 객체를 관리하면서 의존 관계를 맺을 때 개발자가 아닌 Spring에서 객체 간 의존성을 주입해 줍니다.
이것을 DI라고 합니다.
그러면 쉽게 말하면 개발자는 그냥 클래스를 작성하고 해당 클래스를 Bean 객체로 등록해 준다면 Spring에서 해당 객체를 알아서 싱글톤으로 생성하고 가지고 있다가 개발자가 사용하기 위해 코드 몇 줄 딱 작성하면 알아서 의존 객체를 넣어준다는 것이죠
그리고 보면 알 수 있듯이 Spring 프레임워크는 개발자가 신경 써야 할 자잘한 부분들을 최소한으로 줄여주고 서비스와 같은 메인 개발에 집중할 수 있도록 해줍니다.
그래서 어려운 개념이지만 결국 개발자가 확장성이나 유지보수성을 고려하면서 코드를 실수 없이 잘 작성해 주게 도와주는 개발 방식이라는 것을 기억해 두시고 이해하고자 하면 좀 더 쉽게 이해하실 수 있을 것입니다.
1장을 작성하고 되게 늦게 2장을 작성했는데 다음에는 어떤 주제를 작성할지는 모르겠지만, 그때는 빠르게 작성할 수 있도록 노력해 보겠습니다.
'Backend > Spring - 이론' 카테고리의 다른 글
| [Spring] Spring은 왜 사용하는걸까? + 공부할 때 꿀팁 (0) | 2026.04.01 |
|---|---|
| [Spring] - DI (Dependency Injection) - 1 (0) | 2025.10.28 |