"책임은 변경하려는 이유" - 로버트 C.마틴
하나의 클래스는 하나의 책임만을 가져야 한다. 즉, 하나의 클래스는 하나의 이유만으로 변경되어야한다.
너무나도 당연시 여겨지는 내용이지만, 의도치 않게 위반하는 경우가 생긴다. 예시를 들어보자.
- Controller - Service - Repository 구조의 프로젝트에서, Service에서 예외가 발생했을 때 Http 상태 코드를 담은 예외를 던지는 경우
-> 이 경우, Controller가 Http 요청을 받지 않도록 수정되면 Service도 함께 수정되어야하므로 SRP가 위반된다.- Service가 여러개의 Repository와 이를 각각 사용하는 메서드를 한 클래스에 가지고 있는 경우
-> 응집도가 떨어지고, 파편화된 메서드와 필드들이 섞여 있으므로 SRP가 위반된다.
확장에는 열려있어야 하나 변경에는 닫혀있어야 한다.
OCP를 지키는 코드는 클라이언트 코드가 추상화에 의존하고 있기 때문에 확장될 때와 변경될 때 모두 다른 코드에 영향을 주지 않는다.
2 종류의 Repository 클래스가 존재하고, Service에서 if-else 문을 통해 상황에 따라 다른 레포지토리를 참조하는 경우
public class UserService {
private MySQLUserRepository mySQLRepository;
private MongoUserRepository mongoRepository;
public UserService(MySQLUserRepository mySQLRepository, MongoUserRepository mongoRepository) {
this.mySQLRepository = mySQLRepository;
this.mongoRepository = mongoRepository;
}
public User getUser(int id, String dbType) {
if ("MySQL".equals(dbType)) {
return mySQLRepository.findById(id);
} else if ("MongoDB".equals(dbType)) {
return mongoRepository.findById(id);
}
throw new IllegalArgumentException("Invalid dbType");
}
...
}
위의 경우, Repository 종류가 추가/변경될 때, UserService의 코드를 수정해야하므로 OCP를 위반하는 경우이다.
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUser(int id) {
return userRepository.findById(id);
}
...
}
따라서 UserRepository 인터페이스를 사용하여 분기를 없애고, 사용하는 Repository의 종류는 외부에서 결정하도록 하는 것이 좋다.
참고로 OCP를 설명할 때 인터페이스를 이용한 예시가 가장 일반적이지만, 꼭 인터페이스가 필요한 것은 아니며 Enum, 디자인패턴, 이벤트 기반 프로그래밍 등을 통해 OCP를 지킬 수 있다.
파생 클래스는 기반 클래스를 대체할 수 있어야한다. 쉽게말하면, 부모 클래스가 할 수 있는 행동은 자식 클래스도 할 수 있어야 한다는 원칙이다.
리스코프 치환 원칙이 성립하지 않는 경우
- 부모클래스가 할 수 있는 동작을 자식 클래스가 할 수 없는 경우
-> 매번 부모 클래스인지, 자식 클래스인지 분기를 나누어 코드를 작성해야하므로 다형성을 전혀 활용할 수 없다.- 자식클래스에서 필드나 메소드의 접근제어자를 더 엄격하게 변경하는 경우 (public -> private)
-> 자식 클래스의 접근제어자가 더 엄격해지면, 부모클래스 레퍼런스 변수에 자식 클래스의 인스턴스를 할당했을 때 문제가 발생하므로 이는 불가능하다.
따라서 리스코프 치환원칙을 지키지 않는 상속은 하면 안된다.
참고: 계약에 의한 설계
특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.
하나의 인터페이스에 여러 개의 구현 클래스를 위한 메소드를 각각 정의해놓으면, 어느 한 쪽에서는 불필요한 메서드가 노출되는 상황이 발생한다.
이 경우에는 인터페이스를 구현 클래스별로 세분화하면, 불필요한 메소드가 노출되는 상황을 막을 수 있고 유지보수를 편리하게 할 수 있다.
고수준 컴포넌트는 저수준 컴포넌트에 의존하지 않아야 한다.
일반적으로, 구현 클래스에 직접 의존하지 않고 인터페이스를 중간에 끼워넣어 의존 관계를 역전시킬 수 있다.
OCP와 DIP 모두 인터페이스를 사용하는 것이 대표적인 방법이지만, 그 목적에 차이가 있다.
OCP: 확장, 변경 시 다른 코드들이 영향을 받지 않도록 하기 위함
DIP: 인터페이스를 사용하여 고수준 컴포넌트가 저수준 컴포넌트에 의존하지 않도록 바꾸기 위함
간접적으로 의존 역전 원칙이 깨지는 상황
예외를 처리할 때, 고수준 컴포넌트가 처리하는 예외가 저수준 컴포넌트의 정보를 간접적으로 담고 있는 경우
-> 저수준 컴포넌트가 변경되면 고수준 컴포넌트의 예외 처리를 변경해야하므로 OCP / DIP를 모두 위반하게 된다.
따라서, 저수준 컴포넌트(구현체)들이 추상적인 예외를 던지도록 캡슐화하고 고수준 컴포넌트는 하나의 예외만을 처리하도록 변경해야 한다.