Inversion of Control와 Dependency Injection 간소화
Source: Dev.to
제어 역전 (IoC)
제어 역전(IoC)은 모든 현대 프레임워크가 기반으로 삼는 기본 원칙입니다. 핵심 아이디어는 간단합니다: 프레임워크에게 맡기세요.
Spring 같은 백엔드 프레임워크를 사용할 때, 프레임워크는 여러분이 직접 작성해야 할 많은 책임들을 대신 수행합니다:
- 서버 라이프사이클 관리
- 스레드와 동시성 관리
- 각 요청 라우팅
- I/O 스트림 읽기·쓰기
- 데이터 직렬화·역직렬화
- 예외 처리
- 이 모든 것에 대한 테스트 도구 제공
이것이 IoC입니다: 제어가 여러분의 코드에서 프레임워크로 이동합니다.
IoC를 적용하는 프레임워크 예시
- Django (Python)
- Express.js (Node)
- Gin / Gorilla (Go)
의존성 주입 (DI)
IoC와 달리, 의존성 주입(DI)은 디자인 패턴입니다. 프레임워크 없이 DI를 사용할 수도 있고, 프레임워크와 함께 사용하지 않을 수도 있습니다. DI의 목표는 의존성 제공이라는 구체적인 문제를 해결하는 것입니다.
클래스가 다른 클래스에 구현된 무언가를 필요로 할 때, 의존성이 발생합니다. 예를 들어:
public class EmailSender {
private final EmailWriter emailWriter;
public EmailSender(EmailWriter emailWriter) {
this.emailWriter = emailWriter;
}
public void sendEmail(EmailContent emailContent) {
var email = emailWriter.writeEmail(emailContent);
// etc etc
}
}
위 예시에서 EmailSender는 EmailWriter에 의존합니다; 의존성을 제공하지 않으면 NullPointerException이 발생합니다.
DI 없이: 두 가지 수동 해결책
외부에서 의존성 생성
public class SomeService {
public void notifySomething(Destiny destiny) {
var emailWriter = new EmailSender(new EmailWriter());
EmailContent emailContent = buildEmailContent(destiny);
emailWriter.sendEmail(emailContent);
}
}
메서드 안에서 의존성 생성
public class EmailSender {
public void sendEmail(EmailContent emailContent) {
var emailWriter = new EmailWriter();
var email = emailWriter.writeEmail(emailContent);
// etc etc
}
}
두 접근법 모두 다음과 같은 문제를 가집니다:
- 불필요한 결합
- 테스트 어려움
- 의존성을 모킹하기 어려움
- 객체 생성 책임이 “이동”
DI는 이러한 문제를 프레임워크에 의존성 관리를 위임함으로써 해결합니다.
사용 사례: 비디오 API
사용자가 비디오를 업로드하는 API가 있다고 가정해 봅시다. 우리는 다음을 해야 합니다:
- 암호화
- 게시
- 메타데이터 저장
추가 요구사항:
- MP4와 FLV 두 종류의 비디오 지원
- 개발 환경에서는 로컬에 저장하고, 운영 환경에서는 클라우드 제공자를 사용
- 서비스 코드를 수정하지 않고도 제공자를 교체하고 싶음
if (isProd()) { … } else { … }와 같은 조건문을 쓰는 대신, 프로파일을 활용한 DI를 사용합니다.
공통 인터페이스
public interface VideoPublisher {
URI publishContent(VideoPostRequest videoPostRequest);
}
로컬 구현
@Component
@Profile("local")
public class LocalVideoPublisher implements VideoPublisher {
private static final Logger log = LoggerFactory.getLogger(LocalVideoPublisher.class);
@Override
public URI publishContent(VideoPostRequest videoPostRequest) {
log.info("We save the video locally");
return URI.create("file:///local/test/video123");
}
}
운영 구현
@Component
@Profile("prod")
public class CloudVideoPublisher implements VideoPublisher {
private static final Logger log = LoggerFactory.getLogger(CloudVideoPublisher.class);
@Override
public URI publishContent(VideoPostRequest videoPostRequest) {
log.info("Posting to cloud provider!");
return URI.create("https://mycloudprovider.com/uploads/video123");
}
}
인터페이스를 사용하는 서비스
@Service
public class VideoService {
private final VideoPublisher videoPublisher;
private final VideoRepository videoRepository;
public VideoService(VideoPublisher videoPublisher, VideoRepository videoRepository) {
this.videoPublisher = videoPublisher;
this.videoRepository = videoRepository;
}
public UUID postVideo(VideoRequest request) {
URI link = videoPublisher.publishContent(request.toPostRequest());
// persist metadata with the link …
return videoRepository.save(new Video(link, …));
}
}
프로파일을 활용한 테스트
로컬 환경 테스트
@SpringBootTest
@ActiveProfiles("local")
class LocalVideoServiceTest {
@Autowired VideoService videoService;
@Autowired VideoRepository videoRepository;
@Test
void shouldSaveLocally() {
var input = new VideoRequest(
VideoType.MP4,
new byte[]{},
"My title",
"My description",
1L
);
var result = videoService.postVideo(input);
var actual = videoRepository.findById(result);
assertThat(actual).get()
.extracting(Video::link)
.isEqualTo(URI.create("file:///local/test/video123"));
}
}
운영 환경 테스트
@SpringBootTest
@ActiveProfiles("prod")
class ProdVideoServiceTest {
@Autowired VideoService videoService;
@Autowired VideoRepository videoRepository;
@Test
void shouldSaveOnCloudProvider() {
var input = new VideoRequest(
VideoType.MP4,
new byte[]{},
"Some other video",
"Significant description",
2L
);
var result = videoService.postVideo(input);
var actual = videoRepository.findById(result);
assertThat(actual).get()
.extracting(Video::link)
.isEqualTo(URI.create("https://mycloudprovider.com/uploads/video123"));
}
}