Study/Java

[Java] 객체 지향 설계의 5가지 원칙 (SOLID)

jonghne 2023. 10. 2. 19:18

개요

이번 게시글에서는 객체 지향 프로그래밍의 5가지 원칙인 SOLID에 대해 설명합니다. 

 

SOLID란 ?

SOLID란, 클린 코드로 유명한 로버트 마틴이 정리한 좋은 객체 지향 설계을 하기 위한 원칙으로, 

다음과 같이 5가지 원칙으로 이루어져 있다.

 

 
  1. SRP 단일 책임 원칙 (Single Responsibility Principle)
  2. OCP 개방-폐쇄 원칙 (Open/Closed Principle)
  3. LSP : 리스코프 치환 원칙 (Liskov Substitution Principle)
  4. ISP : 인터페이스 분리 원칙 (Interface Segregation Principle)
  5. DIP 의존관계 역전 원칙 (Dependency Inversion Principle)
 

 

SRP 단일 책임 원칙

"하나의 클래스는 하나의 책임만 가져야 한다" 는 원칙이다.

 

만약 아래의 같이 MemberService 클래스에서 MemberRepository의 구현 클래스에 대한 의존 관계를 직접 설정한다면, 구현체가 MemoryRepository -> JpaRepositry로 변경 될 때 MemberService의 코드가 수정된다. 

public class MemberService() {
   private MemberRepository memberRepository = new MemoryRepository();
}
public class MemberService() {
   // private MemberRepository memberRepository = new MemoryRepository();
   private MemberRepository memberRepository = new JpaRepository();
}

 

이는 MemberService 가 멤버에 편의 기능에 대한 책임과 MemberRepository의 의존 관계를 관계 설정에 대한 책임 두가지를 가지게 되기 때문에 SRP 원칙을 어겼다고 할 수 있다.

 

 

OCP 개방-폐쇄 원칙

확장에는 열려 있고 변경에는 닫혀 있게 개발해야 한다 라는 원칙이다

 

아래 예시와 같이 MemberService에서 MemberRepository를 구현체를 변경할 때, MemberService 또한 변경되기 때문에  OCP 원칙을 어겼다고 할 수 있다.

public class MemberService() {
   // private MemberRepository memberRepository = new MemoryRepository();
   private MemberRepository memberRepository = new JpaRepository();
}

 

이런 경우에는 DI를 통해 외부에서 MemberService에 대한 구현체를 주입해주는 방법으로 구현체를 갈아 끼워서 OCP 원칙을 지킬 수 있다.

 

 

LSP 리스코프 치환 원칙

서브 타입은 항상 기반 타입(Base Type)으로 변경될 수 있어야 한다는 원칙으로, 하위 클래스가 기반 클래스에서 정의한 기능을 위반하지 않고 확장해야 한다는 의미이다.

 

여기서 기반 타입/클래스는 객체 상속 계층 구조에서상위에 위치한 클래스 또는 인터페이스를 말하고, 서브 타입/클래스는 기반 클래스 또는 인터페이스를 확장한 클래스를 말한다. 

 

이 LSP 원칙은 기반 클래스나 인터페이스에서 정의한 메서드 시그니쳐나 의도를 어기지 말고 구현 해야 한다는 의미인데, 예를 들면 자동차 인터페이스에서 엑셀 기능을 정의했는데 하위 클래스에서 엑셀 기능을 멈추는 기능으로 구현한다면 LSP 원칙을 어긴 것이다.

 

클라이언트에서 다형성 과 상속을 사용할 때는 인터페이스/기반 클래스의 규약을 믿고 하위 구현 클래스를 사용하는 것이기 때문에 LSP 원칙이 지켜지지 않는다면 클라이언트는 인터페이스/클래스를 신뢰하지 못하게 된다.

 

ISP 인터페이스 분리 원칙

클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다는 원칙으로, 특정 인터페이스에 너무 많은 기능이 포함되어 있다면 차라리 인터페이스를 여러개로 분리하는 것이 좋다는 의미이다.

 

만약 자동차에 대한 인터페이스를 만들려고 할 때 운전, 정비, 외형 정보 등 너무 많은 기능을 자동차 인터페이스 1개로 만들면, 너무 많은 기능이 들어가 있기 때문에 인터페이스의 의미가 모호해질 수 있다. 

 

그렇기 때문에 자동차 인터페이스 1개를 운전 / 정비 / 외형 정보 인터페이스 등으로 나누어서 분리하는게 명확하고 대체 가능성이 높아진다. (ex) 클라이언트를 운전자 클라이언트, 정비사 클라이언트로 분리할 수 있고 정비 인터페이스가 변경되는 경우에도 운전자 클라이언트는 영향이 가지 않는다)

 

또한 너무 많은 기능이 하나의 인터페이스에 존재한다면 클라이언트가 사용하지 않는 메서드에 의존하는 케이스가 많아진다. 이는 사용하지 않는 메서드의 변경이 일어나면 해당 메서드로 인해 클라이언트에 영향이 가기 때문에 좋지 않다.

 

DIP 의존관계 역전 원칙

의존 관계를 맺을 때 구현체가 아닌 추상화에 의존해야 한다는 원칙이다.

 

이 말은 클라이언트 코드는 구체적인 구현 클래스가 아닌 인터페이스나 기반 클래스에 의존해야 한다는 의미이다. 

 

만약 클라이언트 코드에서 인터페이스나 기반 클래스에 의존하지 않고, 구현 클래스에 의존한다면 다른 구현 클래스로 변경해야 하는 경우 클라이언트 코드에 직접적인 변경이 발생되기 때문이다.

 

예시로 아래의 코드는 클라이언트 코드에서 인터페이스가 아닌 구현 클래스인 MemberRepository에 직접 의존하고 있기 때문에 DIP 원칙을 위반한다.

public interface Repository() {
	...
}

public class MemberRepository implements Repository() {
	...
}

public class MemberService() {
   private MemberRepository memberRepository = new MemoryRepository();
}

 

아래와 같이 인터페이스를 의존하게 하고, 외부에서 구현 클래스를 주입하게 한다면 구현 클래스를 갈아 끼워도 MemberService에 변경이 일어나지 않는다.

public class MemberService() {
   private final Repository repository;

    public MemberService(Repository repository) {
        this.repository = repository;
    }
}

 

 

출처 : https://www.inflearn.com/course/스프링-핵심-원리-기본편