Web/SrpingBoot

스프링과 객체 지향 프로그래밍 - SOLID(객체 지향 설계의 5가지 원칙) 관점

나는나는용 2024. 5. 5. 02:14
728x90

SOLID

SRP : 단일 책임 원칙

Single Responsibility Principle
한 클래스는 하나의 책임만 가져야 한다.

 

큰 책임이든, 작은 책임이든, 

변경이 있을 때 파급 효과가 적어야한다.

 

변경이 있어도 다른 곳에는 영향을 끼치지 않게

계층을 잘 나누는 것은 

단일 책임 원칙을 지키기 위함이다. 

 

 

OCP : 개방-폐쇄 원칙

Open/Closed Principle
소프트웨어 요소는 확장에는 열려있으나, 변경에는 닫혀있어야 한다.

 

java에서의 다형성을 활용하여 OCP를 지킬 수 있다.

 

역할과 구현의 분리를 생각해보자.

인터페이스(역할)를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현할 수 있다.

 

확장에는 열려있다 : 인터페이스를 구현한 새로운 클래스는 얼마든 만들 수 있다.

변경에는 닫혀있다 : 인터페이스는 변경 없다.

 

 

문제점

분명 다형성은 잘 지켰다.

확장에는 열려있다라는 것도 잘 지켰다.

 

그런데,

 

MemberService 클라이언트가 구현 클래스를 직접 선택해야하는데,

코드를 변경해야한다.

 

변경에는 닫혀있어야되는데.................????????

public class  MemberService {

//기존코드
// private MemberRepository memberRepository = new MemoryMemberRepository(); 
    
    //변경코드
    private MemberRepository memberRepository = new JdbcMemberRepository();

}

 

구현 객체를 변경하려면 클라이언트 코드를 변경해야한다.

(코드 변경 없이는 구현 객체를 MemoryMemberRepository에서 JdbcMemberRepository로 변경할 수 없다.)

 

분명 다형성을 사용했지만, OCP 원칙('변경에는 닫혀있을 것')을 지킬 수 없다.

 

해결방법

객체를 생성하고, 연관관계를 맺어주는, 조립을 해주는, 별도의 설정자가 필요하다.

 

그것이 바로 스프링!(스프링 컨테이너)

DI, IOC컨테이너도 필요함.

 

추후 다룰 예정

 

 

LSP : 리스코프 치환 원칙

Liskov Substitution Principle
프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야한다.

 

다형성을 지원하기 위한 원칙.

다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다.

인터페이스를 구현한 구현체를 믿고 사용하기 위해 필요한 원칙.

 

단순히 컴파일 된다고해서 넘어가면 안된다.

리스코프 치환 원칙을 만족하며 컴파일이 되어야한다.

 

예시

그 코드가 자동차 액셀의 기능을 구현한 코드라고 가정해보자.

근데, 그 코드는 액셀을 밟으면 후진을 하게끔 하는 구현이라고 해보자.

 

코드상 문제는 없으므로 컴파일이 된다.

그런데, 이 자동차의 '액셀' 클래스는 실제 '액셀 인터페이스'의 규약을 지켰는가?

 

아니다.

'액셀을 밟으면 속력을 내며 자동차를 앞으로 나아가게 한다'는 액셀 인터페이스의 규약을 지키지 않았다.

 

즉, 리스코프 치환 원칙을 만족하지 않은 '액셀' 구현이다. 이 자동차 못탄다.

 

리스코프 치환 원칙을 만족하는 '액셀' 구현이 탑재된 자동차는 믿고 탈 수 있겠다.

 

 

ISP : 인터페이스 분리 원칙

Interface Segregation Principle
특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.

 

기능에 맞게 적당한 크기로 잘 쪼개자.

인터페이스가 명확해지고, 대체 가능성이 높아진다.

 

예시

'자동차 인터페이스' -> '운전 인터페이스', '정비 인터페이스' 로 분리

'사용자 클라이언트' -> '운전자 클라이언트', '정비사 클라이언트' 로 분리

 

정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않는다.

 

DIP : 의존관계 역전 원칙

Dependency Inversion Principle
구현 클래스에 의존하지 말고, 인터페이스에 의존하자.

 

의존성 주입

DIP를 따르는 방법 중 하나.

구체화에 의존하지 말고, 추상화에 의존하자.

 

실제 세상 : 역할에 의존해야된다는 것과 같은 맥락.

역할을 어느 배우가 수행하는지보다, 그 역할을 수행할 배우가 있는게 더 중요하다.

배우가 누가 되더라도, 배우를 변경해도, 역할극 진행에 문제가 생기지 않는다.

 

객체 세상 : 인터페이스에 의존해야한다.

클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다.

 

 

추가

다시,

위에서 살펴보았던 코드를 다시 꺼내보자.

public class  MemberService {

//기존코드
// private MemberRepository memberRepository = new MemoryMemberRepository(); 
    
    //변경코드
    private MemberRepository memberRepository = new JdbcMemberRepository();

}

 

앞서 OCP에서 설명한 MemberService는 구현 클래스를 직접 선택했어야했다.

분명 인터페이스에 의존하지만, 구현 클래스도 동시에 의존한다.

이는 DIP 또한 위반하게 된다.

 

OCP에서 말했듯이

다형성을 잘 지켰기에 확장에 열려있지만,

레파지토리를 직접 선택해야해서, 구현체를 직접 선택해야해서

어떤 선택을 하느냐에 따라 코드를 변경해야했기에,

변경에 닫혀있지 않았다.

 

객체지향의 핵심은 다형성이라고 하지 않았는가...?

개발할 때 코드를 부품 갈아끼우듯이 개발해야 좋다고했는데,

 

다형성만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경되는 것으로 보아,

다형성만으로는 OCP, DIP를 지킬 수 없다.

 

뭔가 더 필요하다. -> 스 프 링

 

스프링과 객체지향이 짝꿍인 이유

좋은 객체 지향 개발을 하기 위해 OCP, DIP 원칙들을 지키면서 개발을 해보면,
결국 스프링 프레임워크(정확히는 DI 컨테이너)를 만들게 된다. 

 

스프링은 다음 기술로 다형성 + OCP, DIP를 가능하게 지원한다.

  • DI(Dependency Injection) : 의존관계, 의존성 주입.
  • DI 컨테이너 제공 

▶ 클라이언트 코드의 변경 없이 기능 확장 가능

 쉽게 부품 교체하듯이 개발 가능

 

정리

아무튼,

모든 설계에 역할구현분리하자.

 

이상적으로는,

모든 설계에 인터페이스를 부여할 것!

 

그런데,,,,

인터페이스를 도입하면 '추상화'라는 비용이 발생함.

▶ 인터페이스(추상화)를 구현한 구체 클래스를 열어봐야됨.

 

기능을 확장할 가능성이 없다면, 구체 클래스를 직접 사용하고,

만약 향후에 꼭 확장이 필요해지면, 리팩터링해서 인터페이스를 도입하는 것도 방법이다.

 

백퍼천퍼만퍼 기능 확장할 아이라면 인터페이스를 부여해서 개발 진행하기.

 

 

728x90