SRP (Single Responsibility Principle) - 단일 책임 원칙
OCP (Open-Closed Principle) - 개방 폐쇄 원칙
LSP (Liscov Substitution Principle) - 리스코프 치환 원칙
ISP (Interface Segregation Principle) - 인터페이스 분리 원칙
DIP (Dependency Inversion Principle) - 의존 관계 역전 원칙
유지보수가 쉬워진다
확장성이 좋아진다.
재사용성이 상승한다.
자연적인 모델링이 가능해진다.
클래스 단위로 모듈화 해서 대형 프로젝트 개발이 용이해진다.
public class LoggingService {
private DataSource loggingDB = new MySQLDataSource();
//로그를 출력하고 저장하는 비즈니스 로직
...
}
위의 코드를 보시면 LoggingService는 로그를 저장하는 저장소인 loggingDB를 사용하고 있습니다.
해당 코드를 작성할 때 개발자가 추후 데이터베이스의 변경이 있을 수 있다고 예상하여, DataSource 인터페이스를 만들고, 그 구현체인 MySQL 데이터베이스를 사용하는 MySQLDataSource 구현체를 만들어서 사용하고 있습니다.
잘 설계된 것처럼 보이지만, 이는 자세히 보면 단일 책임 원칙을 위반하고 있습니다.
loggingDB 객체를 new 키워드를 사용해 직접 생성하고 있다는 점에 주목해야 합니다.
new 키워드의 사용으로 인해 LoggingService는 2가지 역할을 갖게 됩니다.
이는 SRP, 단일 책임 원칙에 위배됩니다.
클래스와 메서드는 하나의 역할만 하도록 한다.
로그를 찍고 저장하는 역할 외에도 로그를 저장하는 데이터베이스까지 스스로 생성하고 있습니다.
즉, 단일 책임이 아니라 2가지의 책임을 갖고 있는 겁니다.
클래스와 메서드는 한 이유로만 변경되어야 한다.
현재 코드를 보면 loggingDB는 DataSource의 구현체인 MySQLDataSource를 사용하고 있습니다.
추후 MySQL 데이터에비스가 아닌 MongoDB를 사용하게 된다면 어떻게 수정해야 할까요?
public class LoggingService {
private DataSource loggingDB = new MongoDBDataSource();
//로그를 출력하고 저장하는 비즈니스 로직
...
}
LoggingService 클래스가, 내부 비즈니스 로직 때문이 아닌, 외부 클래스의 구현의 변경으로 인해 변경되었습니다.
즉, LoggingService는 하나 이상의 이유로 변경되고 있습니다.
LoggingService가 SRP, 단일 책임 원칙을 위반하고 있으니 해결해보겠습니다.
Spring Framework는 위 문제를 해결해주는 기능인 DI(Dependency Injection)를 제공하고 있습니다.
LoggingService를 수정해보겠습니다.
public class LoggingService {
@Autowired
private DataSource loggingDB;
//로그를 출력하고 저장하는 비즈니스 로직
...
}
⭐️ 참고 ⭐️
물론 필드 주입 방식보다 생성자 주입 방식이 권장되지만, 예제니까 넘어가도록 하겠습니다. 자세한 내용은 아래 링크를 참고해주세요
DataSource라는 인터페이스를 구현하는 MongoDBDataSource 클래스를 작성하고, Bean으로 등록해서 Bean Container에 담아두고, @Autowired 어노테이션으로 주입받아 사용하면 됩니다.
여기서 주의할 점이 있습니다.
최종적으로 LoggingService는 이렇게 작성됩니다.
@Qualifier 어노테이션을 사용하지 않고, 설정 파일을 작성해서 어느 구현체를 주입할지 명시해줬다고 가정합니다.
public class LoggingService {
@Autowired
private DataSource loggingDB;
//로그를 출력하고 저장하는 비즈니스 로직
...
}
이제 LoggingService 클래스는 더 이상 loggingDB가 어느 구현체를 사용하는지 new 키워드를 통해 생성하지 않습니다.
즉 DB 생성에 대한 역할이 사라지고, 비즈니스 로직만 수행하면 되는 단일 책임 원칙을 지키게 되었습니다.
이 상태에서, DataSource를 MongoDB에서 MariaDB로 바꿔야 하는 상황이 발생하면, LoggingService는 변경되지 않고, MariaDBDataSource 클래스를 작성하고, 설정 파일에서 해당 구현체가 주입되도록 수정해주면, LoggingService는 MariaDBDataSource 클래스를 사용하게 됩니다.
즉 LoggingService는 비즈니스 로직 변화가 생기면, 단 한 개의 이유만으로 변경됩니다.
확장엔 열려있고 (Open) , 수정엔 닫혀있자 (Closed)
여기서 확장이란 새로운 기능의 추가, 수정이란 기존 소스코드의 변경을 말합니다.
주로 open은 지키기 쉽지만, closed를 지키기 어렵다는 의견을 많이 듣습니다.
앞서 길게 설명한 예제에서, 수정 전의 코드가 SRP을 위반하는 동시에 OCP를 위반하는 코드입니다.
public class LoggingService {
private DataSource loggingDB = new MySQLDataSource();
//로그를 출력하고 저장하는 비즈니스 로직
...
}
새로운 데이터베이스를 추가하게 되면 (확장), MongoDBDataSource라는 클래스를 작성하면 됩니다.
이는 확장에 열려있는 상태입니다.
단, MongoDBDataSource 클래스를 다 작성하고 나면, LoggingService 소스코드를 수정해줘야 합니다.
이는 변경에도 열려있는 상태입니다.
private DataSource loggingDB = new MongoDBDataSource();
앞서 언급한 예시와 같이 다음과 같이 코드를 작성하면 OCP 원칙도 지키게 됩니다.
public class LoggingService {
@Autowired
private DataSource loggingDB;
//로그를 출력하고 저장하는 비즈니스 로직
...
}
B가 A의 자식 타입이면, 부모 타입인 A객체는 자식 타입인 B로 치환해도 작동에 문제가 없어야 한다
리스코프 치환 원칙이 가장 이해하기 어렵고 와닿지 않는 원칙이었습니다.
리스코프 치환 원칙은 코드상에서의 설계보다 객체지향적 설계 부분에서 적용되는 원칙입니다.
class Rectangle {
int width;
int height;
public int getArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public int getArea() {
return width * width;
}
}
//넓이를 계산하는 어플리케이션 비즈니스 로직
public int getArea(Rectangle rectangle) {
rectangle.setHeight(10);
rectangle.setWidth(20);
return rectangle.getArea();
}
@Test
@Displayname("직사각형 클래스, 정사각형 클래스 넓이 구하는 로직 테스트")
public void testArea() {
Rectangle rec = new Rectanlge();
Rectangle square = new Sqaure();
getArea(rec); //결과 : 200
getArea(square); //결과 : 100
}
이 테스트 코드에서 볼 수 있듯이, 기존 애플리케이션에 직사각형 대신에 정사각형으로 치환하면 결과가 달라지며 오류가 발생합니다.
여기서 리스코프 치환 원칙에 위배되는것을 확인할 수 있습니다.
이는 직사각형과 정사각형 간의 상속 관계 설계 오류입니다.
둘이 특성이 다른데, 정사각형이 직사각형을 상속받아서 생기는 오류입니다.
public class Shape {
public int getArea();
}
public class Square extends Shape {
private int width;
@Override
public int getArea() {
return width * width;
}
}
public class Rectangle extends Shape {
private int width;
private int height;
@Override
public int getArea() {
return width * height;
}
}
리스코프 치환 원칙은 초기에 객체 지향 설계 시 제대로 된 설계가 된다면 위반할 가능성이 현저히 낮아집니다.
또 다른 표현으로, 완벽한 Is-a 관계가 되도록 상속 구조를 정의해야 합니다.
다른 회사 정보들을 불러와서 우리 회사 시스템에 저장해야 하는 애플리케이션을 개발해야 할 수요가 생겼습니다.
한 회사의 정보만 불러올게 아니라, 여러 다양한 회사 정보를 불러와야 하기 때문에 인터페이스를 설계하고, 다양한 구현체를 개발하기로 객체 지향적 설계를 했습니다.
그 인터페이스가 DataManger 인터페이스입니다.
public interface DataManager {
//데이터 불러옴
public void laod();
//데이터 준비
public void prepare();
//데이터 저장
public void save();
}
그러다 어느 날, A 회사 데이터를 확인해야 하는 서비스를 구현할 요구사항이 생겼습니다.
단, 이 서비스는 데이터를 불러오기만 하고 확인만 하지, 저장은 하지 않습니다.
해당 서비스를 ACompDataService라고 하고 다음처럼 작성하겠습니다.
ACompDataService 클래스는 DataManager를 상속받기 때문에 load, prepare, save 메서드를 모두 구현해야 합니다.
public class ACompDataService implements DataManager {
//데이터 불러옴
@Override
public void laod() { ... }
//데이터 준비
@Override
public void prepare() { ... }
//데이터 저장
@Override
public void save() { ... }
}
public interface DataLoader {
public void load();
}
public interface DataSaver {
public void save();
}
public interface DataPreparer {
public void prepare();
}
로그를 찍고 저장하는 LoggingService 기억하시나요?
리팩토링 하기 전의 LoggingService는 이렇게 구현되어있었습니다.
public class LoggingService {
private DataSource loggingDB = new MySQLDataSource();
//로그를 출력하고 저장하는 비즈니스 로직
...
}
그중 주목해야 할 소스 코드는 여기입니다.
private DataSource loggingDB = new MySQLDataSource();
여기서 DataSource 인터페이스는 추상화, MySQLDataSource는 추상화를 구현한 구체화입니다.
LoggingService는 현재 코드에 의하면 추상화, 구체화 모두 의존하고 있습니다.
DIP, 의존 관계 역전 원칙을 위배한 것입니다.
앞서 말한 것처럼 Spring에서 제공해주는 기능으로 아래와 같이 코드를 수정합니다.
public class LoggingService {
@Autowired
private DataSource loggingDB;
//로그를 출력하고 저장하는 비즈니스 로직
...
}
이렇게 객체 지향 설계 원칙 5가지인 SOLID에 대해 알아보았습니다.
- SRP
클래스와 메서드는 하나의 역할만 하도록 한다.
클래스와 메서드는 한 이유로만 변경되어야 한다.- OCP
확장엔 열려있고 (Open) , 수정엔 닫혀있자 (Closed)- LSP
B가 A의 자식 타입이면, 부모 타입인 A객체는 자식 타입인 B로 치환해도 작동에 문제가 없어야 한다- ISP
클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 된다.
인터페이스는 하나의 동작을 위해 존재해야 한다.- DIP
추상화에 의존해야 한다. 구현체에 의존하면 안 된다.