본문 바로가기
Java Spring/Spring과 객체지향

[스프링과 객체지향 2편] 객체지향 설계의 5가지 원칙(SOLID)

by 그냥노깡 2022. 7. 3.

 

좋은 객체지향 설계를 위해 따르는 5원칙을 SOLID라고 한다.

SRP, OCP, LSP, ISP, DIP의 앞 글자를 따서 얻어진 이름이다.

이 다섯 가지의 원칙을 지키면, 확장과 변경에 유연하고 유지보수가 쉬운 소프트웨어를 개발할 수 있다.

 

SOLID와 Spring이 무슨 상관이냐고 물을 수 있다.

결론부터 말하면,

SOLID를 지켜서 코드를 작성하면 유지보수가 쉬운 것은 사실이지만,

작성해야 할 코드량이 굉장히 많아져서 때로는 배보다 배꼽이 큰 상황이 생길 수 있다.

Spring Framework는 이런 상황을 해결해주기 위한 다양한 기능을 제공해준다.

하나의 예시로, 스프링 프레임워크의 의존관계 자동 주입 기능은

OCP와 DIP를 지키기 위해 사용하는 Config Class의 코드량을 엄청나게 줄여준다!

 

이 글에서는,

1. SOLID 개념을 하나씩 살펴보고, 

2. OCP와 DIP를 지키기 위한 어려움을 소개할 것이다.

 

SOLID 살펴 보기

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

말 그대로 하나의 모듈은 하나의 책임만을 가져야 한다는 원칙이다.

중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적어야 한다.

 

객체의 생성과 초기화 작업을 분리하거나,

사용자 뷰와 관련된 로직과 비즈니스 로직을 분리하는 등을 SRP를 지킨 예시로 들 수 있다.

 

작은 범위에서 함수 하나는 오직 하나의 일만 수행하도록 구현하고,

큰 범위에서 인터페이스 또는 클래스가 하나의 책임만을 가지도록 설계하는 것이 중요하다.

 

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

리스코프 치환 원칙은 올바른 상속 관계의 특징을 정의하기 위해 만든 원칙이다.

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

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

인터페이스를 구현한 구현체를 믿고 사용하려면, 이 원칙이 필요하다.

 

쉽게 말해, 같은 인터페이스를 구현한 서로 다른 구현체는,

내부 구현이 다를지라도, 특정 메서드는 의미적으로 같은 일을 하도록 구현해야 한다는 원칙이다.

 

예를 들어, 자동차 인터페이스의 엑셀이라는 기능을 '앞으로 가는 것'으로 정의했다면,

구현이 바뀌었을 때 해당 함수가 자동차를 후진하도록 만들거나, 아예 다른 기능을 하도록 바뀌는 것처럼

아예 의미가 변하는 것 LSP를 위반한 것이다.

 

ISP (Interface Segregation Principle), 인터페이스 분리 원칙

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

 

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

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

분리하면 정비 인터페이스 자체가 바뀌어도 운전자 클라이언트에 영향을 주지 않음.

 

이 원칙을 지키면 인터페이스가 보다 더 명확해지고, 대체 가능성이 높아진다.

 

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

추상화에 의존해야지, 구체화에 의존하면 안된다는 원칙이다.

쉽게 말해 정적인 코드 상에서는 구현 클래스에 의존해선 안되고, 인터페이스에 의존하라는 뜻이다.

 

인터페이스에 의존하면, 내부 구현이 바뀌거나 모듈이 다른 모듈로 대체되어도 외부에 영향을 주지 않게 된다.

 

'의존관계 역전'이라고 표현한 이유는,

코드 상에서 인터페이스에 의존하지만, 다형성에 의해 실행시점에는 구현체에 의존하게 되면서

의존관계가 인터페이스에서 구현체로 역전되기 때문이다.

 

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

확장에는 열려있고, 변경에는 닫혀있다.

소프트웨어 요소를 새롭게 확장해도 클라이언트의 변경이 필요하지 않아야 한다는 의미이다.

 

다형성을 사용하고 DIP를 지킴으로써, OCP를 지킬 수 있다.

 

DIP와 OCP: 다형성만으로는 지킬 수 없다.

 

다형성을 지킨 코드인 아래 코드를 보자.

public class MemberService {
	private MemberRepository memberRepository = new MemoryMemberRepository();
    //private MemberRepository memberRepository = new JdbcMemberRepository(); //구현체 변경
}

MemberService 클래스는 MemberRepository라는 인터페이스와 그 구현체인 MemoryMemberRepository에 동시에 의존하고 있다.

구현체에 직접 의존하고 있으므로, 만약 구현체 자체가 바뀌게 되면 클라이언트인 MemberService에도 변경이 필요하다.

즉, 이 코드는 OCP와 DIP를 위반한다.

 

이 문제를 해결하기 위해서, 아래 코드처럼 구현체에 의존하지 않고 인터페이스만 적었다고 가정하자.

public class MemberService {
	private MemberRepository memberRepository;
}

이렇게 되면, 당연하게도 내부에서 memberRepository를 사용할 때 NPE(NullPointerException)가 발생한다.

실행 시점에 구현체가 무엇인지 모르기 때문이다.

 

다음으로

이처럼, 순수 다형성만으로는 OCP와 DIP를 지키면서 개발할 수 없다.

다형성을 통해 인터페이스와 구현을 분리할 수 있지만, 역할의 분리는 수행할 수 없다. (비즈니스 로직 수행 역할, 의존관계 설정 역할) 

역할을 분리하고 OCP와 DIP를 지키기 위해 DI, IoC라는 개념이 등장한다.

 

다음 글에서 DI, IoC에 대해 설명하겠다.

 


참고 자료

망나니 개발자 블로그, SOLID

https://mangkyu.tistory.com/194

스프링 핵심 원리  - 기본편, 김영한

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8

객체지향의 사실과 오해, 조영호

http://www.yes24.com/Product/Goods/18249021