1. SRP: 단일 책임 원칙 (Single Responsibility Principle)
- 핵심: 한 클래스는 하나의 책임만 가져야 한다.
- 판단 기준: 가장 중요한 기준은 변경이다. 변경이 발생했을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것이다.
- 예시: UI 변경, 객체의 생성과 사용을 분리하는 것 등.
2. OCP: 개방-폐쇄 원칙 (Open/Closed Principle)
- 핵심: 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀(변경하지 않아도 된다) 있어야 한다.
- 방법: 다형성을 활용하여 인터페이스를 구현한 새로운 클래스를 만들어 기능을 확장한다.
- 문제점: MemberService가 구현 클래스를 직접 선택하는 경우(예: new MemoryMemberRepository()), 구현 객체 변경 시 클라이언트 코드도 변경해야 하므로 OCP를 완전히 지키기 어렵다.
public class MemberService {
private MemberRepository memberRepository = new MemoryMemberRepository();
}
// MemoryMemberRepository에서 JdbcMemberRepository로 바꿀 시에, Servcie 코드도 수정해야한다
// -> 이는 OCP를 완전히 지키지 못한 것
public class MemberService {
// private MemberRepository memberRepository = new MemoryMemberRepository();
private MemberRepository memberRepository = new JdbcMemberRepository();
- 해결: 객체를 생성하고 연관관계를 맺어주는 별도의 조립·설정자가 필요하다. → DIP
3. LSP: 리스코프 치환 원칙 (Liskov Substitution Principle)
- 핵심: 프로그램의 객체는 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
- 의미: 하위 클래스는 인터페이스 규약을 완벽히 지켜야 한다. 단순히 컴파일 성공을 의미하는 것이 아니라, 인터페이스의 의도대로 동작해야 함을 뜻한다.
- 예시: 자동차 인터페이스의 엑셀은 '앞으로 가기'이다. 이를 '뒤로 가기'로 구현하면 LSP 위반이다. 느리더라도 앞으로 가야 원칙에 부합한다.
4. ISP: 인터페이스 분리 원칙 (Interface Segregation Principle)
- 핵심: 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
- 장점: 인터페이스가 명확해지고 대체 가능성이 높아진다.
- 예시: 자동차 인터페이스 → '운전'과 '정비'로 분리, 정비 인터페이스가 변해도 운전자 클라이언트에 영향을 주지 않는다.
5. DIP: 의존관계 역전 원칙 (Dependency Inversion Principle)
- 핵심: "추상화에 의존해야지, 구체화에 의존하면 안 된다." 즉, 구현 클래스가 아닌 인터페이스(역할)에 의존해야 한다.
- 문제점: 위 OCP 예시의 MemberService는 인터페이스뿐만 아니라 구현 클래스도 동시에 의존하고 있어 DIP를 위반하고 있다.
- 의의: 인터페이스에 의존해야 클라이언트가 유연하게 구현체를 변경할 수 있다.
// 여기서는 Main에서 했지만, 실제에선 보통 Config 파일에서 이러한 설정을 함
public class Main {
public static void main(String[] args) {
// 상황 1: 메모리 저장소를 쓰고 싶을 때
MemberRepository repo1 = new MemoryMemberRepository();
MemberService service1 = new MemberService(repo1);
// 상황 2: JDBC(DB) 저장소로 바꾸고 싶을 때
// ★ 중요: 이때 MemberService의 코드는 단 한 줄도 수정하지 않습니다!
MemberRepository repo2 = new JdbcMemberRepository();
MemberService service2 = new MemberService(repo2);
}
}
public class MemberService {
// 1. 추상화(인터페이스)에만 의존한다. (DIP 지킴)
private final MemberRepository memberRepository;
// 2. 생성자를 통해 외부에서 구현체를 주입받는다. (Dependency Injection)
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public void join(Member member) {
memberRepository.save(member);
}
}
💡 총정리
- 객체 지향의 핵심은 다형성이다.
- 하지만 다형성만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경되는 문제를 완벽히 해결할 수 없다.
- 따라서 다형성만으로는 OCP와 DIP를 완전히 지킬 수 없으며, 이를 해결하기 위한 추가적인 메커니즘이 필요하다.
- 위의 코드에서 Config에서 하던 일(DI 컨테이너 만들기)를 하다보면서, 나온 것이 현재의 Spring 프레임워크이다.(Bean, Autowired 등으로 이를 편하게 구현할 수 있게 함)