SOLID 원칙이란 객체지향 설계에서 지켜줘야 할 5개의 소프트웨어 개발 원칙(SRP, OCP, LSP, ISP, DIP)을 말한다.
객체지향 프로그래밍언어인 Java의 개발자라면 반드시 알아야 할 OOP의 4가지 특징과 더불어 가장 기본적이고 핵심적인 개념이다. 또한 디자인패턴들 또한 이 개념에 근거하여 만들어지기 때문에, 표준화 작업에서부터 아키텍처 설계에 이르기까지 애플리케이션 전역에 광범위하게 적용되는 개념이다.
설계 원칙을 신경쓰지 않고 개발을 해도 정상작동한다면 문제될 것이 없어 보이지만, 그런 방식으로 개발된 코드를 한번이라도 봤다면 설계원칙이 왜 중요한지 뼈저리게 느꼈을 것이다.
설계원칙을 잘 지켜서 개발된 애플리케이션은 시스템에 새로운 요구사항이나 변경사항이 있을 때 영향을 받는 범위가 적다. 그래서 시스템에 예상하지 못한 변경사항이 발생하더라도, 유연하게 대처하고 이후에 확장성이 있는 시스템 구조를 만들 수 있다.
또한, SOLID 객체 지향 원칙을 적용하면 코드를 확장하고 유지 보수 관리하기가 더 쉬워지며, 불필요한 복잡성을 제거해 리팩토링에 소요되는 시간을 줄임으로써 프로젝트 개발의 생산성을 높일 수 있다.
단일 책임 원칙은 하나의 클래스(객체)는 단 하나의 책임만 가져야 한다는 원칙이다. 여기서 말하는 책임이란 기능을 말한다.
Spring framework를 사용하면 MVC패턴을 주로 사용하는데, 이렇게 계층을 나누고 역할을 분배하는 것도 책임을 분배하기 위함이다.
3-tier architecture 또한 표현계층, 비즈니스계층, 데이터계층을 분리하여 각각의 책임범위내에서만 기능하기 위해 나눠진 구성방식이다.
이처럼 책임범위를 명확히하면 수정사항이 있을 때, 한 파일에서 특정 부분을 변경하고 또 그로인해 다른 부분을 변경하고 또 다른 부분을 변경해야하는 일을 없애 유지보수성을 높여준다.
이 때 책임범위는 개발자마다 또는 애플리케이션의 특성에 따라 달라질 수 있다.
만약 가게라는 객체가 있다고 가정해보자. 가게에는 종업원과 요리사, 청소담당직원이 있다.
이 때, 요리사는 요리 기능만 잘 구현되어 있으면 된다. 만약 요리사가 계산 기능, 청소 기능 까지 있다면 사용성이 더 좋을 수는 있으나, 요리사가 휴무라면 청소 및 계산까지 할 수 없게 되기 때문에 문제가 생긴다.
개방 폐쇄 원칙은 클래스는 확장에 열려있고 변경에 닫혀있어야 한다는 원칙이다.
즉, 클래스의 확장(extends)을 사용해 이후에 기능을 추가하기 용이한 구조로 만들되 다른 클래스의 수정사항에 영향을 받아 수정되어야 하지 않도록 설계되어야 한다는 뜻이다.
예를들어 강아지라는 객체가 있다고 해보자. 강아지는 '멍멍'하고 짖기도 하고 사료를 먹기도 한다.
이 때 고양이라는 새로운 객체가 추가되었다. 고양이는 '야옹'하고 울기도하고 츄르를 먹는다.
수많은 동물이 있고 각각 특성에 맞는 기능이 필요하다면 공통된 부분은 동물 클래스로 묶고 동물 클래스를 상속받아 자신의 특성에 맞는 기능은 하위 클래스에서 구현한다면 수정이 발생했을 때 다른 클래스에 영향도는 없애면서 공통 기능을 활용해 효율적으로 개발할 수 있다.
abstract class Animal {
public abstract void speak();
public void eat() {
System.out.println("사료먹기");
}
}
class Dog extends Animal {
public void speak() {
System.out.println("멍멍");
}
// 구현하지 않은 메소드는 부모 메소드를 실행한다.
}
class Cat extends Animal {
public void speak() {
System.out.println("야옹");
}
public void eat() {
System.out.println("츄르먹기");
}
}
class Main {
static public void main(String []args) {
Animal animal = new Dog();
animal.speak();
animal.eat();
animal = new Cat();
animal.speak();
animal.eat();
}
}
#출력내용
멍멍
사료먹기
야옹
츄르먹기
리스코프 치환 원칙은 자식클래스는 부모클래스 타입으로 선언할 수 있다는 원칙이다.
즉, 리스코프 치환 원칙은 다형성 원리를 이용하기 위한 원칙 개념으로 보면 된다.
abstract class Animal {
public abstract void speak();
public void eat() {
System.out.println("사료먹기");
}
}
class Dog extends Animal {
public void speak() {
System.out.println("멍멍");
}
// 구현하지 않은 메소드는 부모 메소드를 실행한다.
}
class Cat extends Animal {
public void speak() {
System.out.println("야옹");
}
public void eat() {
System.out.println("츄르먹기");
}
}
class Test {
test() {
Animal animal = new Dog();
eat(animal);
animal = new Cat();
eat(animal);
}
void eat(Animal animal) {
animal.eat();
}
}
위와 같이 똑같은 Animal 타입 변수에 이를 확장하는 Dog 타입과 Cat 타입을 모두 담을 수 있고 또한 Animal 타입을 파라미터로 받는 eat 함수에 전달해줄 수도 있다.
인터페이스 분리 원칙은 인터페이스를 각각 사용에 맞게 끔 잘게 분리해야한다는 설계 원칙이다.
SRP와 꽤 유사한데 이 두 원칙의 차이는 SRP는 클래스의 단일 책임을 강조한다면, ISP는 인터페이스의 단일 책임을 강조하는 것아다.
예를 들어 아래와 같이 스마트폰의 부가기능을 정의하는 interface가 있다고 가정해보자.
참고로 interface는 ~~able의 형태로 명명하는 것이 컨벤션이다.
interface funcable {
/**
* 무선충전 추상 함수
*/
void wirelessCharge();
/**
* AR 추상 함수
*/
void ar();
/**
* 생체인식 추상 함수
*/
void biometrics();
}
그리고 스마트폰 A는 무선충전과 AR이 지원되고 스마트폰 B는 무선충전과 생체인식이 지원된다면 아래와 같이 구현될 것이다.
class Smartphone {}
class A extends Smartphone implements funcable {
@Override
public void wirelessCharge() {
System.out.println("무선충전중입니다.");
}
@Override
public void ar() {
System.out.println("AR 중입니다.");
}
@Override
public void biometrics() {
System.out.println("지원하지 않는 기능입니다.");
}
}
class B extends Smartphone implements funcable {
@Override
public void wirelessCharge() {
System.out.println("무선충전중입니다.");
}
@Override
public void ar() {
System.out.println("지원하지 않는 기능입니다.");
}
@Override
public void biometrics() {
System.out.println("생체인증 중입니다.");
}
}
이렇게 인터페이스가 제대로 책임범위에 맞게 분리되지 않으면 사용하지도 않는 기능을 override해서 구현해야 하는 불편함이 생긴다.
그러므로 아래와 같이 인터페이스를 분리해준다.
interface WirelessChargeable {
/**
* 무선충전 추상 함수
*/
void wirelessCharge();
}
interface ARable {
/**
* AR 추상 함수
*/
void ar();
}
interface Biometricsable {
/**
* 생체인식 추상 함수
*/
void biometrics();
}
이렇게 책임범위를 분리해주면 아래와 같이 사용하는 인터페이스만 상속하여 구현할 수 있다.
class Smartphone {}
class A extends Smartphone implements WirelessChargeable, ARable {
@Override
public void wirelessCharge() {
System.out.println("무선충전중입니다.");
}
@Override
public void ar() {
System.out.println("AR 중입니다.");
}
}
class B extends Smartphone implements WirelessChargeable, Biometricsable {
@Override
public void wirelessCharge() {
System.out.println("무선충전중입니다.");
}
@Override
public void biometrics() {
System.out.println("생체인증 중입니다.");
}
}
의존관계 역전 원칙은 어떤 클래스를 참조해서 사용해야하는 상황이 생긴다면, 그 클래스를 직접 참조하는 것이 아니라 그 대상의 상위 요소로 참조하라는 원칙이다.
쉽게 이야기해서 구현 클래스는 자주 변경이 될 수 있기 때문에 변화가 비교적 거의 없는 상위 요소(인터페이스)에 의존함으로써 결합도를 낮추라는 뜻이다.
예를 들어 햄버거에 돼지고기 패티가 들어가는 객체가 있다고 가정해보자.
class Hamburger {
private PorkPatty portPatty;
public Hamburger(PorkPatty porkPatty) {
this.portPatty = porkPatty;
}
public PorkPatty getPorkPatty() {
return porkPatty;
}
}
class PorkPatty {}
class Test {
public void test() {
PorkPatty patty = new PorkPatty();
Hamburger hamburger = new Hamburger(patty);
System.out.println(hamburger.getPorkPatty());
}
}
이 떄 만약 햄버거가 소고기 패티를 가질 수도 있다고 하면 아래와 같이 변경될 것이다.
class Hamburger {
private PorkPatty portPatty;
private BeefPatty beefPatty;
public Hamburger(PorkPatty porkPatty, BeefPatty beefPatty) {
this.portPatty = porkPatty;
this.beefPatty = beefPatty;
}
public PorkPatty getPorkPatty() {
return porkPatty;
}
public BeefPatty getBeefPatty() {
return beefPatty;
}
}
class Patty {}
class PorkPatty extends Patty {}
class BeefPatty extends Patty {}
class Test {
public void test() {
Patty patty = new PorkPatty();
Hamburger hamburger = new Hamburger(patty, null);
System.out.println(hamburger.getPorkPatty());
patty = new BeefPatty();
hamburger = new Hamburger(null, patty);
System.out.println(hamburger.getBeefPatty());
}
}
햄버거는 소고기패티 또는 돼지고기패티 둘 중 하나만을 가진다는 것을 고려하면 이는 매우 비효율적인 방법이다.
만약 이 두종류의 패티 외에 다른 패티가 추가된다면 계속해서 속성을 늘려야 할 것이다.
그러므로 하위 클래스를 직접 참조하는 것이 아닌 상위 요소를 참조하는 형식으로 변경해준다.
class Hamburger {
private Patty patty;
public Hamburger(Patty patty) {
this.patty = patty;
}
public Patty getPatty() {
return patty;
}
}
class Patty {}
class PorkPatty extends Patty {}
class BeefPatty extends Patty {}
class Test {
public void test() {
Patty patty = new PorkPatty();
Hamburger hamburger = new Hamburger(patty);
System.out.println(hamburger.getPatty());
patty = new BeefPatty();
Hamburger hamburger = new Hamburger(patty);
System.out.println(hamburger.getPatty());
}
}