[JAVA] 객체지향 설계 5원칙 (SOLID)

Coastby·2022년 10월 5일
0

LIKELION Back-End School

목록 보기
32/61

⭐️ SOLID
의미 : 객체 지향 프로그래밍을 하면서 지켜야하는 5대 원칙이다.
목적 : 변경이 용이하고, 유지보수와 확장이 쉬운 소프트웨어를 개발하기 위함이다.

  • SPR (Single Responsibility Principle) : 단일 책임 원칙
  • OCP (Open Closed Principle) : 개방 폐쇄 원칙
  • LSP (Listov Substitution Principle) : 리스코프 치환 원칙
  • ISP (Interface Segregation Principle) : 인터페이스 분리 원칙
  • DIP (Dependency Inversion Principle) : 의존 역전 원칙

○ 단일 책임의 원칙 (SRP, Single Responsibility Principle)

"어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다."

하나의 모듈은 한 가지 책임을 가져야 한다는 것으로, 이것은 모듈이 변경되는 이유가 한 가지여야 함을 의미한다. 해당 모듈이 여러 대상 또는 액터들에 대해 책임을 가져서는 안 되고, 오직 하나의 액터에 대해서만 책임을 가져야 한다.

단일 책임 원칙을 제대로 지키면 변경이 필요할 때 수정할 대상이 명확해진다. 그리고 이러한 장점은 시스템이 커질수록 극대화되는데, 시스템이 커지면서 서로 많은 의존성을 갖게되는 상황에서 변경 요청이 오면 딱 1가지만 수정하면 되기 때문이다.

❌ 나쁜 예

한 사람이 두가지 일을 모두 책임지려고 하고 있다.

public class Person {
	
    public String job;
    public Person(String job)
    {
        this.job = job;
    }
    
    public void Work()
    {
        if(job.equals("Programmer"))
            System.out.println("코딩하다");
        else if(job.equals("Teacher"))
            System.out.println("수업을 하다.");    
    }
}

⭕️ 좋은 예

역할에 따라서 하나의 책임만 가져간다.

public abstract class Person {
    abstract public void Work();
}
 
 
public class Programmer extends Person {
    public void Work()
    {
        System.out.println("개발을 하다");
    }
}
 
public class Teacher extends Person{
    
    public void Work()
    {
        System.out.println("학생을 가르치다");
    }
}

○ 개방 폐쇄 원칙 (Open-Closed Principle, OCP)

확장에 대해 열려있고, 수정에 대해서는 닫혀있어야 한다.

  • 확장에 열려있다 : 요구사항이 변경될 때 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있다.
  • 수정에 닫혀있다 : 기존의 코드(호출하는 코드)를 수정하지 않고 조립코드 (assembly code)만 수정할 수 있다.

원칙을 지키기 위해,

  1. 확장 될 것과 변경을 엄격히 구분한다.
  2. 이 두 모듈이 만나는 지점에 인터페이스를 정의한다.
  3. 구현에 대한 의존보다는 정의된 인터페이스를 의존하도록 코드를 작성한다.
  4. 변경이 발생하는 부분을 추상화하여 분리한다.


대표적으로 JDBC 인터페이스를 이용하여 여러 데이터베이스를 갈아끼울 수 있다.

개방 폐쇄 원칙을 지키기 위해서는 추상화에 의존해야 한다. 추상화란 핵심적인 부분만 남기고, 불필요한 부분을 제거함으로써 복잡한 것을 간단히 하는 것이다. 변하지 않는 부분은 고정하고 변하는 부분을 생략하여 추상화함으로써 (인터페이스 이용) 변경이 필요한 경우 생략된 부분을 수정하여 (구현체 변경) 개발 폐쇄의 원칙을 지킬 수 있다.

이는 결국 런타임 의존성과 컴파일타임 의존성에 대한 이야기이다. 여기서 런타임 의존성이란 애플리케이션 실행 시점에서의 객체들의 관계를 의미하고, 컴파일타임 의존성이란 코드에 표현된 클래스들의 관계를 의미한다. 다형성을 지원하는 객체 지향 프로그래밍에서 런타임 의존성과 컴파일타임 의존성은 동일하지 않다.
컴파일 시점에는 추상화된 인터페이스에 의존하고 있지만 런타임 시점에는 구체 클래스에 의존하게 된다.

객체가 알아야 할 지식이 많으면 결합도가 높아지고, 결합도가 높아질수록 개방 폐쇄 원칙을 따르는 구조를 설계하기가 어려워진다.

추상화를 통해 변하는 것들은 숨기고 변하지 않는 것들에 의존하게 하려고 하자.

○ 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

"서브 타입은 언제나 자신의 기반 타입 (부모) 으로 교체할 수 있어야 한다."

상속 관계에서 자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다는 개념이다. 즉, 객체의 상속관계에서 자식 클래스는 언제든 부모 타입으로 교체할 수 있다는 말을 뜻한다.

객체를 호출하면서 클라이언트는 부모의 어떤 자식이 올 지는 모르지만 최소한으로 기대하는 기능이 있을 것이다. 자식은 이러한 기능은 만족시켜줄 수 있어야한다. 자식의 다양한 기능을 클라이언트가 예상하지 못할 수 있으므로 추상화 레벨을 맞춰서 메소드 호출이 불가능하도록 하거나 해당 추상화 레벨에 맞게 메소드를 오버라이딩 하는게 합리적일 것이다.

Now when we look at how each shape would draw based on their dimensions, we can see how they can each be passed around and act as their parent class.

class Shape

	def initialize(dimension*)
		@dimension = dimension
	end

end

class Circle < Shape

	def initialize(dimension)
 		@radius = dimension
	end

end

class Rectangle < Shape

	def initialize(dimension_1, dimension_2)
		@height = dimension_1
		@width = dimension_2
	end

end

class Square < Shape

	def initialize(dimension)
		@side_length = dimension
	end

end

○ 인터페이스 분리 원칙 (Interface segregation principle, ISP)

"클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다."

각 역할에 맞게 인터페이스를 분리한다. 즉 클라이언트의 목적과 용도에 맞는 기능만을 구현하는 인터페이스를 제공해야 한다.

인터페이스 분리 원칙을 지키기 위해, 어떤 구현체에 부가 기능이 필요하다면 이 인터페이스를 구현하는 다른 인터페이스를 만들어서 해결할 수 있다.

클라이언트에 따라 인터페이스를 분리하면 변경에 대한 영향을 더욱 세밀하게 제어할 수 있다.

❌ 나쁜 예

Car 클래스에 오토드라이빙 메서드가 있지만 이 기능을 사용하지 않는 구현체가 있을 수 있다. 이러한 경우 인터페이스를 세부화하여 나눠주는 것이 원칙에 맞다.

public interface Car {
	
    String autoDrive();    
    String autoParking();    
    String drive();
    String break();    
    
}

public class Ray implements Car {

    //해당 기능을 사용 안하고싶음!!!!!
    @Override
    public String autoDrive() {
        return "";
    }

    //해당 기능을 사용 안하고싶음!!!!!
    @Override
    public String autoParking() {
        return "autoParking";
    }

    @Override
    public String drive() {
        return "drive";
    }
    
    @Override
    public String break() {
        return "break";
    }
}

⭕️ 좋은 예

인터페이스를 나누어서 각자 꼭 필요한 기능만을 가지게 한다.

public interface Car {
	
    String drive();
    String break();    
    
}

public interface ElectricCar {
	
    String autoDrive();    
    String autoParking();    
    
}

그리고 필요에 따라서 인터페이스를 상속하는 것이 좋다.

public class Telsa implements Car,ElectricCar {

    @Override
    public String autoDrive() {
        return "autoDrive";
    }

    @Override
    public String autoParking() {
        return "autoParking";
    }

    @Override
    public String drive() {
        return "drive";
    }
    
    @Override
    public String break() {
        return "break";
    }
}

public class Ray implements Car {

    @Override
    public String drive() {
        return "drive";
    }
    
    @Override
    public String break() {
        return "break";
    }
}

○ 의존 역전 원칙 (Dependency Inversion Principle, DIP)

"고수준 모듈은 저수준 모듈에 의존하면 안된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야한다. 즉, 자신보다 변하기 쉬운것에 의존하지 마라"

  • 고수준 모듈 : 변경이 없는 추상화된 클래스 (또는 인터페이스)
  • 저수준 모듈 : 변하기 쉬운 구체 클래스

의존 관계를 맺을 때 변화하기 쉬운 것보다는 변화하기 어려운 것에 의존하라는 원칙이다. 여기서 변화하기 어려운 것이란 추상적인 객체 (interface, abstract)를 말한다.

의존 역전 원칙은 개발 폐쇄 원칙과 밀접한 관련이 있으며, 의존 역전 원칙이 위배되면 개발 폐쇄 원칙 역시 위배될 가능성이 높다. 의존 역전 원칙에서 의존성이 역전되는 시점은 컴파일 시점이다. 런타임 시점에는 구체 클래스에 의존한다. 하지만 의존 역전 원칙은 컴파일 시점 도는 소스 코드 단계에서의 의존성이 역전되는 것을 의미하며, 코드에서는 인터페이스에 의존한다.

⭕️ 좋은 예

자동차가 구체적인 타이어가 아닌 추상화된 타이어 인터페이스에만 의존하게 함으로써 타이어가 변경되어도 자동차가 영향을 받지 않느다.

참고 :
https://mangkyu.tistory.com/194
https://devlog-wjdrbs96.tistory.com/380
https://jeongkyun-it.tistory.com/108
http://daisymolving.github.io/2016/03/18/liskov-substitution-principle.html

profile
훈이야 화이팅

0개의 댓글