SOLID 5원칙
SRP(Single Responsibility Principle) : 단일 책임 원칙
OCP(Open/Colosed Principle) : 개방 폐쇄 원칙
LSP(Liskov Substitution Principle) : 리스코브 치환 원칙
ISP(Interface Segregatoin Principle) : 인터페이스 분리 원칙
DIP(Dependecy Inversion Principle) : 의존관계 역전 원칙
1. SRP
단일 책임 원칙(Single responsibility principle)
- 한 클래스는 하나의 책임만 가져야 한다.
- 하나의 책임의 중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 SRP를 잘 따른것
ex) UI 변경 시 서버 코드에는 영향을 주지 않음, 객체 사용이 생성에 영향을 끼치지 않도록 함
2. OCP
개방 폐쇄 원칙(Open closed principle)
-소프트웨어 요소는 확장에는 열려있느나 변경에는 닫혀 있어야 한다.
- 다형성을 통해 어느정도 지킬 수 있으나 부족하다. 이를 스프링으로 극복한다.
3. LSP
리스코프 치환 원칙(Liskov substitution principle)
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
- 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는것, 다형성을 지원하기 위한 원칙,
인터페이스를 구현한 구현체를 믿고 사용하려면 필요한 원칙이다.
ex) 엑셀을 밟으면 앞으로 가야하는데 뒤로 가게하면 LSP 위반
4. ISP
인터페이스 분리 원칙(Interface segregation principle)
- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다
- 자동차 인터페이스를 운전 인터페이스와 정비 인터페이스로 나눈다고 생각해보자.
정비 인터페이스를 변경한다고 해도 운전 인터페이스는 영향을 받지않는다.
이처럼 자동차 인터페이스를 하나로 만드는 것보다 여러개로 나누는 것이 효과적이다.
- 인터페이스가 명확해지고, 대체 가능성이 높아진다.
5. DIP
의존관계 역전 원칙(Dependency inversion principle)
- 객체는 추상에 의존해야하며 구체화에 의존하면 안 된다.
- 즉, 객체는 인터페이스에 의존해야지 구현 클래스에 의존하면 안 된다.
SOLID 원칙을 돕는 Spring
1. 다형성을 통한 SOLID에서의 문제점
위에서 설명한 SOLID 중 SRP, LSP, ISP는 개발자가 집중을 가하면 지킬 수 있다.
하지만 OCP와 DIP는 다형성을 통해 어느정도 만족시킬 수 있느나 완벽히 원칙을 지키기가 어렵다.
어떤 부분에서 문제가 발생하는지 아래 코드와 함께 설명하겠다.
public class MemberService {
//private MemberRepository memberRepository = new MemoryMemberRepository(); // 원래 코드
private MemberRepository memberRepository = new JdbcMemberRepository(); // 변경한 코드
}
만약 MemberRepository라는 인터페이스에 구현 클래스로 MemoryMemberRepository, JdbcMemberRepository가 있다고 해보자.
MemberService 클래스에서 MemoryMemberRepository를 구현 객체로 사용하고 있다가
JdbcMemberRepository로 변경하려고 한다.
그런데 현재는 위 코드에서처럼 MemberService 클래스에서 직접 코드 변경을 해줘야만
구현 객체를 변경할 수 있다.
이는 직접적인 코드 변경이 필요한 것이므로 확장에는 열려있으나 변경에는 닫혀있으라는 OCP를 위반한다.
또한 구현 클래스를 직접 MemberService 내부에 명시해주고 있다.
이는 구현 클래스에 의존하는 것으로 추상에만 의존해야하며 구체화에 의존하면 안 된다는 DIP를 위반한다.
2. Spring을 통한 문제 해결
Spring에서는 위의 문제를 DI(Dependency Injectino) 컨테이너를 사용해 해결한다.
(DI 컨테이너를 스프링 컨테이너라고도 부른다.)
1. MemoryMemberRepositoy, JdbcMemberRepository를 빈 객체로 스프링 컨테이너에 생성
@Component
public class MemoryMemberRepository implements MemberRepository{
...(생략)...
}
@Component
public class JdbcMemberRepository implements MemberRepository{
...(생략)...
}
해당 클래스 앞에 @Component 어노테이션을 붙이면 해당 클래스가 DI 컨테이너에 빈 객체로 생성한다.
2. MemberService 변경
@Component
public class MemberServiceImpl{
private final MemberRepository memberRepository;
@Autowired // == ac.getBean(MemberRepository.class), Type을 기준으로 자동 주입해준다.
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...(생략)...
}
원래는 구현 클래스로 어떤 것이 쓰일지 코드에 명시되어 있었는데 사라졌다.
이는 1번에서 MemoryMemberRepository와 JdbcMemberRepository를 DI 컨테이너에 빈 객체로 생성해두었고,
이를 @Autowired라는 어노테이션을 통해 자동 주입해주기 때문에 가능하다.
하지만, 이 코드를 그대로 사용하면 에러가 발생한다.
이유는 MemberRepository와 관련된 빈 객체가 2개가 있어 어떤 것을 사용해야하는지 알 수 없기 때문이다.
이를 해결하기 위해서는 1번에서 추가한 @Component 어노테이션을
Service에서 사용할 클래스 하나에만 남기고 삭제하는 방법이 있다.
하지만 이는 OCP를 위반한다.
(MemberService 클래스에 @Component 어노테이션을 붙여 빈 객체로 만들어야 다른 빈 객체를 주입받을 수 있다.)
3. AppConfig를 통한 클래스 외부에서의 의존 주입 설정
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
위의 문제를 해결하기 위해 외부에서 주입할 객체를 설정해주도록 변경할 수 있다.
먼저 AppConfig라는 클래스를 별도로 생성한다.(클래스 이름은 상관없다.)
@Configuration이라는 어노테이션을 붙여주면 스프링 컨테이너와 관련된 설정 파일 클래스로 사용되는데
다른 클래스에서 이 환경 설정 클래스를 통해 생성한 빈 객체들을 사용할 수 있다.
@Bean 어노테이션을 통해 반환되는 값들을 빈 객체로 생성하고 있다.
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
그 중 위 코드는 memberRepository라는 이름의 빈 객체를 생성한 것으로,
해당 이름의 빈 객체를 사용하면 MemoryMemberRepository 객체가 반환된다.
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
위는 memberService라는 이름의 빈 객체를 생성한 것으로
해당 이름의 빈 객체를 사용하면 MemberService 객체가 반환되는데
memberRepository() 메소드를 통해 MemoryMemberRepository를 주입받은 채로 생성이 된다.
이 방법을 통해 2번에서 발생했었던 OCP 문제도 해결이 가능하다.
물론 MemverService에서 MemoryMemberRepository 대신 JdbcMemberRepository로 구현 객체를 변경하고 싶으면
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new JdbcMemberRepository();
}
}
와 같은 코드 변경이 필요하다.
하지만 어떤 객체를 주입해줘야 한다는 설정을 해주는 설정 클래스에서만 코드 변경이 발생하고
실제 사용되는 MemberService 클래스에서는 변경이 발생하지 않으므로 이는 OCP를 위반하는 것이 아니다.
사용 예시
public class MemberApp {
public static void main(String[] args) {
// 어노테이션 기반 환경설정
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
// memberService라는 메소드로 정의된 빈 가져옴
MemberService memberService = applicationContext.getBean("memberService",MemberService.class);
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find Member = " + findMember.getName());
}
}
전에 만들어놓았던 빈 객체들을 AnnotationConfigApplicationContext 메소드를 사용해 환경설정 클래스에서 받아온다.
위 코드는 memberService라는 이름을 가진 빈 객체를 받아 사용하는 모습이다.
해당 Service 객체는 이전 단계에서 작성한 설정에 따라 MemoryMemberRepository를 사용한다.
제어의 역전 IoC
Inversion of Controll
기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고 실행했다.
즉, 구현 객체가 프로그램의 제어 흐름을 조종했다.
반면 AppConfig 사용으로 구현 객체는 자신의 로직을 실행하는 역할만 담당하게 되는데
즉, 프로그램 제어 흐름을 이젠 구현 객체가 아닌 AppConfig가 가져가게 된다.
이로 인해 구현 객체들은 자신이 의존하는 인터페이스들에 어떤 구현 객체가 오는지도 모르는 상태로
자신의 로직만 실행하게 된다.
이처럼 프로그램의 제어 흐름을 구현 객체가 직접 제어하는 것이 아니라
외부에서 관리하는 것을 제어의 역전 IoC라고 한다.
'스프링 > 스프링 Core basic' 카테고리의 다른 글
스프링 Core - 스프링 컨테이너와 스프링 빈 (0) | 2023.09.06 |
---|