JAVA 공부 5일차
SOLID
1) 목차
- Single Responsibility Principle(SRP) : 단일 책임 원칙
- Open-Closed Principle(OCP) : 개방-폐쇠 원칙
- Liskov Substitution Principle(LSP) : 리스코프 치환 원칙
- Interface Segregation Principle(ISP) : 인터페이스 분리 원칙
- Dependency Inversion Principle(DIP) : 의존성 역전 원칙
ㄱ. Single Responsibility Principle(SRP) : 단일 책임 원칙
하나의 모듈이 하나의 책임을 가져야 한다는 원칙으로 불리지만 모듈이 변경되는 이유가 한가지여야 함으로 받아들이자
이는 해당 모듈이 여러 대상 또는 메서드들에 대한 책임을 가져서는 안되고, 오직 하나의 메서드에 대해서만 책임을 져야한다는 것을 의미한다.
만약 어떤 모듈이 여러 메서드에 대해 책임을 가지고 있다면 여러 메서들로부터 변경에 대한 요구가 올 수 있으므로, 해당 모듈을 수정해야 하는 이유 역시 여러 개가 될 수 있다. 반면 어떤 클래스가 단 하나의 책임만 가진다면 특정 메서드로부터 변경을 특정할 수 이씩에 해당 클래스를 변경하는 이유와 시점이 명확해 진다.
이러한 장점은 시스템이 커질수록 극대화되고, 서로 많은 의존성을 갖게 되는 상황에서 변경 요청이 오면 딱 1가지만 수정하면 되기 때문에 손쉽게 처리가 가능하다.
ex) 사용자의 이름, 이메일, 암호를 받아서 저장한다고 가정했을 때 암호를 암호화하는 로직에 대한 변경이 필요하다고 했을 때, getpassword()로 받아오게 만들어 getpassword()만 수정하면 되도록 한다. getUserData라는 기능안에 모든걸 다 구현했다면 일이 커질 수 있다.
ㄴ. Open-Closed Principle(OCP) : 개방-폐쇠 원칙
확장에 대해 열려있고, 수정에 대해서는 닫혀있어야 한다는 원칙이며 의미는 다음과 같다.
- 확장에 대해 열려있다 : 요구사항이 변경될 때 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있다.
- 수정에 대해 닫혀있다 : 기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있다.
이러한 개방-폐쇠 원칙을 지키기 위해서는 추상화에 의존해야한다. 추상화를 통해 변하지 않는 부분은 고정하고 변하는 부분을 수정하여 개방-폐쇠 원칙을 지킬 수 있다.
- ex) 사용자의 암호화된 비밀번호를 받는다고 했을 때 암호화 정책이 바뀌었다고 가정하자. 단지 passwordEncoder 객체를 통해 받으면 되며, getUserData가 구제적인 암호 클래스에 의존하지 않아도 되며, passwordEncoder 인터페이스에 의존하도록 추상화를 한다면, 개방-폐쇠 원칙을 지키는 코드를 작성할 수 있다.
ㄷ. Liskov Substitution Principle(LSP) : 리스코프 치환 원칙
하위 타입은 상위 타입을 대체할 수 있어야 한다는 원칙이다. 해당 객체를 사용하는 클라이언트는 상위 타입(자신의 기반 타입, basetype)이 하위 타입으로 변경되어도, 차이점을 인식하지 못한 채 상위 타입의 퍼블릭 인터페이스를 통해 서브 클래스를 사용할 수 있어야 한다는 것이다. 즉, 부모 객체를 호출하는 동작에서 자식 객체가 부모 객차렐 완전히 대체할 수 있어야 한다는 원칙이다. 가장 흔한 예시인 사각형을 보자.
- ex) width와 height를 set하여 get 할 수있는 Rectangle 클래스를 선언하고 width와 height를 같은값으로 고정시키는 square 클래스를 Rectangle 클래스로 부터 상속받아 왔다고 한다. 그리고 main 함수에서 rectangle 함수를 호출하고 width 10, height를 5로 주고 넓이를 50이 나온다. 하지만 같은 경우에서 Square함수를 통해 width 10, height를 5을 주면 넓이는 25가 나온다.
위 ex)에서 코드상으로는 맞지만 Square의 기능은 Rectangle의 기능을 대신할 수 없다. 즉 엄격히 따지면 사각형이라는 특징만 같을 뿐, 하나가 다른 하나를 완전히 포함하지는 못하는 구조인 것이다. 리스코프 치환 원칙을 지키기 위해선 가급적 부모 객체의 일반 메소드를 그 의도와 다르게 오버라이딩 하지 않는 것이 중요하다.
ㄹ. Interface Segregation Principle(ISP) : 인터페이스 분리 원칙
클라이언트가 필요하지 않는 기능을 가진 인터페이스에 의존해서는 안되고, 최대한 인터페이스를 작게 유지해야한다. 즉 범용 인터페이스가 아닌 특정 클라이언트를 위한 인터페이스 여러개가 좋다. 클라이언트의 목적과 용도에 적합한 인터페이스만 제공함으로써 모든 클라이언트가 자신의 관심에 맞는 퍼블릭 인터페이스만 접근하여 불필요한 간섭을 최소화 할 수 있으며, 기존 클라이언트에 영향을 주지 않은 채로 유연하게 객체의 기능을 확장하거나 수정할 수 있다.
- ex) 최신 S20을 구현하고 S2를 구현할 경우 무선충전,생체인식과 같은 기능은 제공하지 않지만, 부모객체인 스마트폰에 이러한 인터페이스가 포함되어 있으면 S2입장에서는 필요하지도 않는 기능을 구현하는 낭비가 발생한다. 그렇다면 전화, 문자 같은 기본 기능만 스마트폰 class에 넣은 뒤 무선 충전, 생체인식 등은 각각의 인터페이스에 구현후 S20에서 오버라이딩하여 사용하면 된다.
즉, 클라이언트가 가지는 부담을 덜어주고 딱 필요한 만큼만 작동시키기 위한 원칙이라고 볼 수 있다. 기능을 제한한다고 할 수 있으며, 불필요한 기능의 상속/구현을 최대한 방지함으로써 객체의 불필요한 책임을 제가한다. 큰 규모의 객체는 필요에 따라 인터페이스로 잘게 나누어 확장성을 향상시킨다.
ㅁ. Dependency Inversion Principle(DIP) : 의존성 역전 원칙
저수준 모듈보다 고수준 모듈에 의존해야한다는 원칙이다. 반대로 고수준 모듈은 저수준 모듈에 의존해서는 안된다.
- 고수준 모듈 : 인터페이스와 같은 객체의 형태나 추상적 개념
- 저수준 모듈 : 구현된 객체
즉 객체는 객체보다 인터페이스에 의존해야한다는 의미로 볼 수 있으며, 객체의 상속은 가급적 인터페이스를 통해 이루어져야 한다는 의미로 해석될 수 있다.
- ex) 게임으로 가정해보자. 무기엔 여러 종류가 있지만 OneHandSword를 사용하는 Character를 만들었다고 해보자. 여러 종류의 무기가 있음에도 OneHandSword만 사용할 수 밖에 없다. 그렇기에 Attackable을 만들어 이 클래스를 상속하는 여러 무기중 OneHandSword를 만들면, Character는 OneHandSword외에 다양한 무기를 사용할 수 있을 것이다.
의존성 역전 원칙은 코드의 확장성 및 재사용성을 추구하기 위한 원칙이다. 경직된 객체보다 구현되지 않아 유연한 인터페이스가 더욱 확장 가능성이 높을 것이다. 하지만 이 원칙은 다른 원칙에 비해 중요도가 떨어지는데 이 원칙은 OCP를 지키면 자연스레 준수되기 때문이다. ISP를 준수할 경우에도 마찬가지이다. 하지만 무시하지는 말고 올바른 의존 관계를 구현하도록 해야한다.
4)참조 링크
- 객체지향 5원칙(망나니개발자) : https://mangkyu.tistory.com/194
2. 리스코프 치환 원칙 : 알파카의 개발 낙서장(각종 원칙을 페이지 단위로 정리함)
https://blog.itcode.dev/posts/2021/08/15/liskov-subsitution-principle