의존 역전 원칙(DIP: Dependency Inversion Principle)
의존 역전 원칙(DIP)는 객체가 특정 클래스(Class)를 참조하여 사용하는 상황이 발생했을 때, 그 클래스를 직접 참조하는 대신 상위 요소(추상 클래스 또는 인터페이스)를 통해 참조하라는 원칙입니다.
DIP의 핵심 개념
객체 간에 정보를 주고받는 과정에서는 의존 관계가 형성됩니다. 이때 DIP 원칙은 구체 클래스(Concrete Class)가 아니라 추상 클래스(Abstract Class)나 인터페이스(Interface)를 통해 의존하라고 요구합니다.
즉, 고수준 모듈(비즈니스 로직)이 저수준 모듈(구현체)에 의존하지 않고, 둘 다 추상화된 상위 요소에 의존하도록 설계해야 합니다.
DIP의 정의를 한 문장으로 요약
"상위 인터페이스를 통해 객체와 통신하라."
DIP 적용의 장점
- 변화에 대한 유연성:
구체 클래스의 변경이 상위 요소에 의존하는 코드에 영향을 주지 않음. - 확장성:
새로운 구현체를 추가하더라도 상위 요소에 의존하는 코드에는 영향이 없음. - 테스트 용이성:
의존하는 객체를 Mock 객체로 쉽게 대체하여 테스트 가능.
DIP 원칙의 위반과 해결: 예제
DIP 원칙 위반 예제
다음은 DIP 원칙을 위반한 코드입니다.
음악 플레이어에서 다양한 형식의 오디오 파일을 재생한다고 가정합니다.
- 구체 클래스에 직접 의존하는 구조
class MP3Player {
public void playMP3() {
System.out.println("Playing MP3 file...");
}
}
class MusicPlayer {
private MP3Player mp3Player; // 구체 클래스에 의존
public MusicPlayer(MP3Player mp3Player) {
this.mp3Player = mp3Player;
}
public void play() {
mp3Player.playMP3(); // 구체 클래스의 메서드를 호출
}
}
문제점
- MusicPlayer는 MP3Player에 강하게 결합되어 있으므로, FLACPlayer나 WAVPlayer 같은 새로운 오디오 형식을 지원하려면 코드를 수정해야 합니다.
- 이는 확장성과 유연성이 낮고, 변경에 취약한 코드 구조입니다.
DIP 원칙 준수 예제
1. 인터페이스 도입
다양한 오디오 형식을 아우르는 상위 인터페이스를 정의합니다.
interface AudioPlayer {
void play();
}
class MP3Player implements AudioPlayer {
@Override
public void play() {
System.out.println("Playing MP3 file...");
}
}
class FLACPlayer implements AudioPlayer {
@Override
public void play() {
System.out.println("Playing FLAC file...");
}
}
2.MusicPlayer가 상위 인터페이스에 의존
이제 MusicPlayer는 구체적인 오디오 형식에 의존하지 않고, AudioPlayer 인터페이스에만 의존합니다.
class MusicPlayer {
private final AudioPlayer audioPlayer; // 상위 인터페이스에 의존
public MusicPlayer(AudioPlayer audioPlayer) {
this.audioPlayer = audioPlayer;
}
public void play() {
audioPlayer.play(); // 상위 모듈을 통해 호출
}
}
3. 사용 예제
새로운 오디오 형식을 쉽게 추가하거나 변경할 수 있습니다.
public class Main {
public static void main(String[] args) {
AudioPlayer mp3Player = new MP3Player();
MusicPlayer musicPlayer = new MusicPlayer(mp3Player);
musicPlayer.play(); // "Playing MP3 file..."
// FLAC 형식으로 변경
AudioPlayer flacPlayer = new FLACPlayer();
musicPlayer = new MusicPlayer(flacPlayer);
musicPlayer.play(); // "Playing FLAC file..."
}
}
DIP와 OCP(개방-폐쇄 원칙) 관계
DIP를 지키면 OCP(변경에는 닫히고, 확장에는 열려 있다)도 자연스럽게 준수됩니다.
새로운 오디오 형식을 추가하거나 구현을 변경하더라도 MusicPlayer 클래스에는 영향을 주지 않기 때문입니다.
DIP를 실무에서 보는 사례
1. 컬렉션 프레임워크
컬렉션 사용 시 항상 상위 인터페이스를 사용합니다.
List<String> songs = new ArrayList<>();
Set<String> artists = new HashSet<>();
2. DI(의존성 주입)와 스프링
스프링 프레임워크는 DIP를 기반으로 설계되었습니다.
클래스는 직접 객체를 생성하지 않고, 인터페이스를 통해 의존 관계를 주입받습니다.
결론
DIP는 추상화된 상위 요소를 통해 객체 간 통신하라는 원칙입니다.
이를 통해 유연성, 확장성, 테스트 용이성을 확보할 수 있으며, 실무에서 OCP 및 DI와 긴밀한 관계를 가집니다.
즉, DIP를 준수하면 유지보수하기 쉬운 코드를 작성할 수 있습니다.
'서버&백엔드 > 🔥 JAVA' 카테고리의 다른 글
Java | TDD시 사용되는 Assert (0) | 2024.12.19 |
---|---|
JAVA | enum사용법 (0) | 2024.12.19 |
JAVA | TDD예제를 통한 어댑터 패턴 설명 (0) | 2024.12.19 |
JAVA | POJO로 개발한다는 무슨말일까? (0) | 2024.12.18 |
JAVA | Record란 무엇일까 (0) | 2024.12.18 |