[개발 Q&A] - Spring IoC와 DI 개념
Spring Framework에서 IoC(Inversion of Control)와 DI(Dependency Injection)가 무엇인지 설명하고, 간단한 예시를 들어주세요.
내 답변
IoC는 제어의 역전으로, 개발자가 아닌 프레임워크나 라이브러리에게 대신 복잡한 작업을 위임하는 것이고, DI는 의존성 주입으로, 어떤 클래스에 대해 하나의 객체만을 싱글톤으로 생성하여 여러 곳에서 사용하게끔 하는 방법입니다.
피드백
잘한 점
- IoC(제어의 역전) 개념을 기본적으로 이해하고 있음
- "개발자가 아닌 프레임워크에게 위임"이라는 표현이 올바른 방향임
개선할 점
- DI에 대한 설명이 잘못됨: "하나의 객체만을 싱글톤으로 생성" → 이는 싱글톤 패턴에 대한 설명이며, DI와는 전혀 다른 개념
- 의존성 주입의 핵심을 놓침: 객체가 필요로 하는 의존 객체를 외부에서 주입받는 방식
- DI의 목적 누락: 객체 간의 결합도를 낮추는 것이 핵심 목적
더 나은 답변
IoC: 제어의 역전으로, 객체의 생성과 관리를 개발자가 아닌 Spring 컨테이너가 담당하는 것입니다.
DI: 의존성 주입으로, 객체가 필요로 하는 의존 객체를 직접 생성하지 않고 외부(Spring 컨테이너)에서 주입받는 방식입니다.// DI 없이 public class UserService { private UserRepository repo = new UserRepository(); // 직접 생성 } // DI 적용 @Service public class UserService { @Autowired private UserRepository repo; // 외부에서 주입받음 }
IoC, DI 둘 다 최근에 공부했던 부분이었기에 기억에 남아있는 개념이었다. 실제로도 어느정도 "아, 이런 거구나"하는 개념은 잡혀있기도 했다. 하지만 두 개념에 대해 말로 풀어 설명하려고 하니 턱 하고 말문이 막히는 기분이었다.
결국 전체적으로 두루뭉실한 답변이 될 수밖에 없었는데, IoC에 대해서는 맞았다고 해주었지만 DI 부분에 대한 답변은 완전히 잘못되었다는 피드백이 돌아왔다. 그래서 "그런데 의존성 주입을 하기 위해서는 하나의 객체를 싱글톤으로 생성한 후, 이 싱글톤으로 생성된 객채를 의존성으로 주입하는 거 아니야?"라고 물었더니 성급하게 평가했다며, 갑자기 180도 돌아서서는 " Spring DI의 실제 동작과 정확히 일치합니다!"란다.
뭐랄까. 내 의도는 "내 말이 맞다니까!"가 아니라, 그래도 완전히 틀린 건 아니지 않나..? 하는 거였는데, 갑자기 저렇게 180도 돌변하니 기분이 묘하달까.
그래도 곰곰히 생각해본 결과, 클로드의 첫 번째 답변이 조금 더 옳바른 방향인 것 같다.
1. IoC
Inversion of Conversion. 직역하면 제어의 역전이라고 부른다. 대부분 스프링 공부하면서 처음 접했을 용어겠지만, 비단 Spring에서만 사용하는 용어는 아니다. 제어의 주체를 역전했다, 라고 조금 더 풀어서 쓰면 이해하기 쉬울 것 같기도 하다. 위키백과에서는 IoC에 대해 아래와 같이 설명하고 있다.
제어 반전, 제어의 반전, 역제어는 프로그래머가 작성한 프로그램이 재사용 라이브러리의 흐름 제어를 받게 되는 소프트웨어 디자인 패턴을 말한다. 줄여서 IoC(Inversion of Control)이라고 부른다. 전통적인 프로그래밍에서 흐름은 프로그래머가 작성한 프로그램이 외부 라이브러리의 코드를 호출해 이용한다. 하지만 제어 반전이 적용된 구조에서는 외부 라이브러리의 코드가 프로그래머가 작성한 코드를 호출한다.
핵심은 "외부 라이브러리의 코드가 프로그래머가 작성한 코드를 호출한다"라는 문장이다.
스프링을 이용한 개발 경험이 있다면 @Service와 @Controller와 같은 어노테이션을 떠올려보자. 두 어노테이션을 붙이는것으로 우리는 프로그램에서의 객체 생성과 생성 주기의 관리를 Spring에게 위임하게 된다. 문장으로 이해가 안 된다면, 아래의 코드를 한 번 살펴보자.
🔄 IoC(제어의 역전) 코드 예시
1. IoC 없는 경우 (개발자가 직접 제어)
public class UserService { private UserRepository userRepository; public UserService() { // 개발자가 직접 객체 생성과 관리를 담당 this.userRepository = new UserRepository(); } public void saveUser(String name) { userRepository.save(name); } } public class Main { public static void main(String[] args) { // 개발자가 직접 모든 객체를 생성하고 관리 UserService userService = new UserService(); userService.saveUser("홍길동"); } }
문제점: 개발자가 모든 객체의 생성, 소멸, 의존성 관리를 직접 해야 함
2. IoC 적용 (Spring이 제어 담당)
@Repository public class UserRepository { public void save(String name) { System.out.println(name + " 저장 완료"); } } @Service public class UserService { @Autowired private UserRepository userRepository; // Spring이 주입 public void saveUser(String name) { userRepository.save(name); } } @SpringBootApplication public class Main { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(Main.class, args); // Spring 컨테이너에서 객체를 가져옴 (우리가 생성하지 않음!) UserService userService = context.getBean(UserService.class); userService.saveUser("홍길동"); } }
🎯 핵심 차이점
객체 생성 new UserRepository() @Repository → Spring이 생성 의존성 주입 직접 할당 @Autowired → Spring이 주입 생명주기 관리 개발자 책임 Spring 컨테이너가 관리 제어 주체 개발자 Spring Framework
2. DI
Dependency Injection, 의존성 주입은 IoC에 대해 제대로 이해하고 있다면 쉽게 이해할 수 있는 개념이다. 클로드의 답변을 인용하자면 "객체가 필요로 하는 의존 객체를 직접 생성하지 않고, 외부(Spring 컨테이너)에서 주입받는 방식"이다.
🎯 DI(의존성 주입) 쉬운 설명
1단계: "의존성"이 뭔가요?
public class 커피숍 { private 바리스타 바리스타; // 커피숍은 바리스타에 "의존"함 public String 커피만들기() { return 바리스타.에스프레소만들기(); // 바리스타 없으면 커피 못 만듦 } }
의존성: 커피숍이 바리스타를 반드시 필요로 하는 관계
2단계: "직접 생성" vs "외부 주입"
직접 생성 방식
public class 커피숍 { private 바리스타 바리스타; public 커피숍() { this.바리스타 = new 바리스타(); // 내가 직접 바리스타 고용 } }
외부 주입 방식 (DI)
@Service public class 커피숍 { @Autowired private 바리스타 바리스타; // 사장(Spring)이 바리스타를 배정해줌 // 생성자에서 바리스타를 직접 고용하지 않음! }
3단계: 실제 Spring 코드 예시
DI 없는 코드
public class UserService { private UserRepository userRepository; public UserService() { // 내가 직접 만듦 - 강한 결합 this.userRepository = new UserRepository(); // 만약 UserRepository 생성자가 바뀌면? 이 코드도 수정해야 함! } public void saveUser(String name) { userRepository.save(name); } }
DI 적용 코드@Service public class UserService { @Autowired private UserRepository userRepository; // Spring이 만든 것을 받아옴 // 생성자에서 직접 만들지 않음! // Spring이 알아서 UserRepository 객체를 만들어서 여기에 넣어줌 public void saveUser(String name) { userRepository.save(name); // 그냥 사용만 하면 됨 } } @Repository public class UserRepository { public void save(String name) { System.out.println(name + " 저장"); } }
4단계: 무슨 일이 일어나는가?
// Spring 내부에서 일어나는 일 (자동) @SpringBootApplication public class Application { public static void main(String[] args) { // 1. Spring이 UserRepository 객체 생성 (싱글톤) UserRepository repo = new UserRepository(); // 2. Spring이 UserService 객체 생성하면서 // 위에서 만든 UserRepository를 주입 UserService service = new UserService(); service.userRepository = repo; // 이런 식으로 주입 // 3. 개발자는 그냥 사용만 하면 됨 UserService service = context.getBean(UserService.class); service.saveUser("홍길동"); } }
💡 쉬운 비유
- 직접 생성: 내가 집에서 요리할 때 모든 재료를 직접 사러 가는 것
- 의존성 주입: 배달 음식처럼 필요한 것을 외부에서 가져다주는 것
핵심: 내가 만들지 않고, 남(Spring)이 만든 것을 받아서 쓰는 것이 DI입니다! 🎯
역시 알고 있다고 생각하는 개념도 막상 설명하려고 하면 제대로 안 나오는 것 같다. 누군가에게 가르칠 수 있을 정도는 되어야 제대로 아는 거라고 하던데, 그 말에 100번 공감한다.