본문 바로가기
Java/Spring

좋은 객체 지향 설계의 5가지 원칙(SOLID)

by oneny 2023. 6. 22.
SOLID
클린코드로 유명한 로버트 마틴이 좋은 객체 지향 설계의 5가지 원칙을 정리

 

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

 

SRP(Single Responsibility Priciple)
  - 단일 책임 원칙

  • 한 클래스는 하나의 책임만 가져야 한다.
  • 하나의 책임이라는 것은 모호하다.
    • 클 수 있고, 작을 수 있다.
    • 문맥과 상황에 따라 다르다.
  • 중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것이다.
    • 예) UI 변경, 객체의 생성과 사용을 분리

 

OCP(Open/Closed Priciple)
  - 개방-폐쇄 원칙

  • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
  • 다형성을 활용하여 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현

 

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

  • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 다형성을 지원하기 위한 원칙, 인터페이스를 구현한 구현체는 믿고 사용하려면, 이 원칙이 필요하다.
  • 단순히 컴파일에 성공하는 것을 넘어서는 이야기

 

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

  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
    • 자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스로 분리
    • 사용자 클라이언트 -> 운전자 클라이언트, 정비사 클라이언트로 분리
  • 분리하면 정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않음
  • 인터페이스가 명확해지고, 대체 가능성이 높아진다.

 

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

  • 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나다.
  • 쉽게 이야기해서 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 의미이다.
  • 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다!
  • 구현체에 의존하게 되면 변경이 아주 어려워진다.

 

DIP, OCP 위반

위 CategoryService 클래스에서 OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수한 것처럼 보이지만 그렇지 않다.

  • DIP: 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.
  • OCP: 지금 코드를 MemoryCategoryRepository -> JdbcCategoryRepository로 변경하려면 클라이언트 코드에 영향을 준다.

CategoryService에서 관심사 분리를 통해 구현 클래스는 전혀 모르고 추상 클래스에만 의존하게 만들어 문제를 해결하자. 

 

관심사의 분리

애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들어 관심사 분리를 해보자.

먼저, CategoryService에서 CategoryRepository를 DI 사용하도록 생성자를 추가한다.

AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다. 그리고 AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 위에서 추가한 생성자를 통해 주입(연결)하여 관심사를 분리할 수 있다. 설계 변경으로 CategoryService는 MemoryCategoryRepository를 더 이상 의존하지 않는다! 단지 CategoryRepository 인터페이스만 의존하고, CategoryService의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다. 이렇게  DI(Dependencies Injection)을 사용하면 기존 코드를 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있어 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중할 수 있다.

좋은 객체 지향 설계 5가지 원칙 중 SRP(Single Responsibility Priciple, 단일 책임 원칙)을 따르면서 관심사를 분리했다. 그리고 MemoryCategoryRepository를 다른 리포지토리 JdbcCategoryRepository로 변경하기로 위해서 스프링 빈을 사용해보자. 스프링 빈을 등록하는 방법에는 아래와 같이 2가지가 있다.

 

스프링 빈과 의존관계

위는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용하려고 한다.

스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성) 정보로 사용한다. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 이라 한다. 개발자는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용한다. 아래는 스프링 빈으로 등록하는 2가지 방법이다.

 

스프링 빈을 등록하는 2가지 방법

  • 컴포넌트 스캔과 자동 의존관계 설정
  • 자바 코드로 직접 스프링 빈 등록하기

컴포넌트 스캔을 이용하는 방법은 @Component 애노테이션이 있으면 스프링 빈으로 자동 등록되는데 이는 나중에 보기로 하고, 자바 코드로 직접 스프링 빈을 등록하는 방법을 살펴보자.

참고: 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다(유일하게 하나만 등록해서 공유한다). 따라서 같은 스프링 빈이면 모두 같은 인스턴스이다. 설정으로 싱글톤이 아니게 설정할 수 있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.

 

자바 코드로 직접 스프링 빈 등록하기

DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둔다. 그래서 DI를 만들 수 있다.

이렇게 스프링의 DI(Dependencies Injection)을 사용하여 CategoryService 클래스 기존 코드를 전혀 손대지 않고, 설정만으로 구현클래스를 위 코드처럼 변경하였다. 그리고 위에서 살펴본 개방-폐쇄 원칙(OCP, Open-Closed Principle)과 DIP(Dependency Inversion Principle)을 지키면서 개발을 하는 것을 확인할 수 있다.

 

컴포넌트 스캔과 자동 의존관계 설정을 통한 Controller 등록

@Component 애노테이션이 있으면 스프링 빈으로 자동 등록된다. @Controller 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문이다. @Component를 포함하는 다음 애노테이션도 스프링 빈으로 자동 등록된다.

  • @Controller
  • @Service
  • @Repository

그리고 컨트롤러에서 리턴 값으로 문자를 반환하면 뷰 리졸버(viewResolver)가 화면을 찾아서 처리한다.

  • 스프링 부트 템플릿엔진 기본 viewName 매핑
  • resources:template/ + {viewName} + .html