스프링의 SOLID 원칙
SOLID 원칙
SRP (Single Responsibility Principle): 단일 책임 원칙
OCP (Open Closed Priciple): 개방 폐쇄 원칙
LSP (Listov Substitution Priciple): 리스코프 치환 원칙
ISP (Interface Segregation Principle): 인터페이스 분리 원칙
DIP (Dependency Inversion Principle): 의존 역전 원칙
대표적인 객체지향언어 기반의 스프링 핵심으로 여겨지는 원칙이다.
스프링은 객체지향 언어인 자바를 어떻게 활용할까?
SRP - 각 클래스(객체)는 단 한가지의 책임 만을 갖는다.
하나의 클래스는 하나의 기능만을 담당한다. 만약 한가지 객체에서 다른 클래스의 코드를 관리한다면 프로그램의 유지, 보수에 많은 비용이 들어갈 것이다. 남배우가 여배우의 연기에 디렉팅을 하면 안된다.
쉽게 말하면 클래스를 생성할 땐 확실한 아이덴티티 하나만을 가지고 만들어야한다는 뜻.
OCP - 클래스는 '확장에 열려있어야 하며, 수정에는 닫혀있어야 한다'
단순하게 설명해서, 구체화에 의존하지 않고 추상화에 의존해야 한다는 뜻. 다형성과 확장에 용이한 객체 설계의 장점을 십분 활용하고자 하는 원칙
백날 공부한 인터페이스에 관한 서술. 인터페이스에 의존한 코드를 작성하면, 구체화 클래스를 새롭게 확장하기 편하다. 이런 과정에서 기능 확장을 해도 관련된 인터페이스는 수정할 필요가 없어야 한다.
LSP - 서브 타입은 언제나 기반(부모) 타입으로 교체가능
인터페이스를 효과적으로 이용하려면 객체를 갈아끼울 때, 문제가 발생하지 않게 즉 유연한 프로그래밍을 진행할 수 있게 해주는 원칙.
ISP - 인터페이스를 각각 사용에 맞게 끔 잘게 분리해야한다는 설계 원칙
뚱뚱한 인터페이스 만들기 금지, 명확하고 깔끔한 인터페이스 설계 후 상속받는 자식 클래스를 만드는 구조가 필요함.
DIP - 의존 역전 원칙
객체 간의 관계를 최대한 느슨하게 해주는 원칙. 어떤 클래스가 다른 클래스를 참조해야 할 때, 그 클래스의 하위 클래스보다 상위 클래스(인터페이스)에 의존한다는 내용이다.
코드로 이해하기
예시로 이해 해보자
1. 구현체 내부에 구현체 자체 생성
OrderServiceImpl 라는 클래스가 있고 이 클래스의 내부에 FixDiscountService라는 내부 클래스가 존재할 때, 일반적으로는 그냥 new를 이용해서 인스턴스를 주입하면 되지 않을까? 라고 생각한다.
interface DiscountService{
int discountOn(int money);
}
class RateDiscountService implements DiscountService{
@Override
public int discountOn(int money){
int discountmoney=(int)Math.round(money*0.1);
return money-discountmoney;
}
}
class FixDiscountService implements DiscountService{
@Override
public int discountOn(int money){
int discountmoney=1000;
return money-discountmoney;
}
}
public class OrderServiceImpl implements OrderService {
FixDiscountService fixDiscountService= new FixDiscountService();
}
이러면 물론 문제될 것은 없다. 문제는 discountservice를 변경할 때 발생하는 데, 만약 fix로 고정된 할인이 아닌 비율 할인을 적용하고 싶다면 어떻게 해야할까?
RateDiscountService 클래스를 새롭게 만들고, OrderServiceImpl 이라는 클래스 내부를 전부 새롭게 갈아 끼워야 할 것이다. 이미 앞서 OCP에서 설명한 확장에 열리지도 않고, 수정에 닫혀있지도 않은 원칙이다.
애초에 인터페이스를 도입할 이유가 없다.
그러면 FixDiscountService 객체가 아닌 부모 인터페이스인 DiscountService로 변경하고, RateDiscountService 객체를 주입해보자
2. 인터페이스 변수에 구체화 객체 주입
interface DiscountService{
int discountOn(int money);
}
class RateDiscountService implements DiscountService{
@Override
public int discountOn(int money){
int discountmoney=(int)Math.round(money*0.1);
return money-discountmoney;
}
}
class FixDiscountService implements DiscountService{
@Override
public int discountOn(int money){
int discountmoney=1000;
return money-discountmoney;
}
}
public class OrderServiceImpl implements OrderService {
DiscountService discountService=new RateDiscountService();
}
이러면 된걸까? 인터페이스 변수 선언 후, RateDiscountService를 자체적으로 주입했다.
안된다. 어쨌든 RateDiscountService라는 구체화 인스턴스에 OrderServiceImpl에서 자체적으로 접근했기 때문이다.
확장에 용이하고 수정에 닫혀있는가?
-> No. 확장하려면 구현 클래스에 수정이 필요함.
3. 인터페이스 변수에 구체화 객체를 넣어주는 (의존성 주입) AppConfig 클래스 생성
interface DiscountService{
int discountOn(int money);
}
class RateDiscountService implements DiscountService{
@Override
public int discountOn(int money){
int discountmoney=(int)Math.round(money*0.1);
return money-discountmoney;
}
}
class FixDiscountService implements DiscountService{
@Override
public int discountOn(int money){
int discountmoney=1000;
return money-discountmoney;
}
}
public class OrderServiceImpl implements OrderService {
DiscountService discountService;
AppConfig appConfig=new AppConfig();
discountService=appConfig.discountService();
discountService.discountOn(20000) // = 18000
}
//AppConfig 클래스, 관심사의 분리
class AppConfig {
public DiscountService discountService(){
return new RateDiscountService();
}
public OrderService orderService(){
return new OrderServiceImpl(discountService());
}
}
이제 더 이상 OrderServiceImpl 클래스에는 discountService 인터페이스의 구체화 객체 생성은 어디에도 없다.
AppConfig라는 구성요소를 담당하는 클래스에게 주입받기 때문이다.
즉, 할인 정책을 비율에서 고정식으로 바꾸고 싶다면 AppConfig 클래스의 discountService 함수만 반환 객체를 바꾸면
수정 끝!!!
그럼 끝인가?
-> No. 모든 구성요소에 의존성을 주입하고 싶으면 매 클래스에 AppConfig 를 선언하고 새로운 객체를 주입해야함
= 메모리 낭비
귀찮음을 해결하고자 스프링 탄생, 이때까진 전부 자바 언어만 이용한 것임. 이제서야 프레임워크 등장
4. 스프링을 이용한 의존성 주입
interface DiscountService{
int discountOn(int money);
}
class RateDiscountService implements DiscountService{
@Override
public int discountOn(int money){
int discountmoney=(int)Math.round(money*0.1);
return money-discountmoney;
}
}
class FixDiscountService implements DiscountService{
@Override
public int discountOn(int money){
int discountmoney=1000;
return money-discountmoney;
}
}
public class OrderServiceImpl implements OrderService {
DiscountService discountService;
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
discountService = applicationContext.getBean("discountService", DiscountService.class);
discountService.discountOn(20000) // = 18000
}
//AppConfig 클래스, 관심사의 분리
@Configuration // 스프링만의 어노테이션
class AppConfig {
@Bean
public DiscountService discountService(){
return new RateDiscountService();
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(discountService());
}
}
AppConfig 클래스 (DI 컨테이너) 에 새로운 스프링 전용 어노테이션이 붙었다.
이제 우리는 Bean을 통한 의존성 주입은 전적으로 스프링 프레임워크에 맡긴다. (제어의 역전)
이 ApplicationContext 컨테이너에서 주입받는 Bean은 싱글톤 원칙을 따른다.
쉽게 말해서 객체를 한번 생성하고 저장한 뒤, 주입을 필요로 할 때마다 그 객체를 주는 것이다.
(매번 return new로 생성하지 않음)
이를 통해 우리는 메모리를 효율적으로 관리 가능해졌다. (더해서 몇가지 부가 기능도 제공해줌)
But! class 의존 관계가 많아지고 복잡해지면 귀찮다 (귀찮음을 위해 생기는 무언가들..)
-> 컴포넌트 스캔의 등장
여기까지 다루면 호흡이 너무 길어져 이 뒤는 이후에 작성하겠다.
정리하자면
구체화 클래스간의 관계를 걷어내기 위해, AppConfig를 통해서 연결을 분리하고,
이 과정의 효율성과 단순화를 스프링에 맡긴다.
'스프링' 카테고리의 다른 글
Spring Security 기술로 인증/인가 커스텀하기 (0) | 2024.08.25 |
---|---|
스프링과 JWT(Json Web Token) 기술을 이용한 인증과 인가 구현 (0) | 2024.08.06 |
<인프런> 실전! 스프링 부트와 JPA 활용1 - 새로이 배운 것 (0) | 2024.07.18 |
[Docker] SpringBoot + PostgreSql docker-compose 쉽게 하기 (0) | 2024.05.28 |
[Docker] 도커를 이용한 스프링 부트 배포 (0) | 2024.05.28 |