스프링을 잘 이해하기 위해서는 먼저 객체지향 설계의 5개의 원칙을 알아야 된다고 합니다.
오늘은 객체지향 설계의 5개의 원칙 중 스프링을 설명하기 위해 꼭 필요한 원칙과 그 원칙의 의미를 한 번 알아보며, 스프링으로 어떻게 풀어나가는지 한 번 알아보겠습니다.
1. 객체지향 설계의 5개 원칙 - SOLID
객체지향 설계의 5개의 원칙을 SOLID 라고 부릅니다. SOLID는 아래 5개의 원칙을 의미합니다.
- SRP : 단일 책임 원칙 (Single Responsibility Principle)
- OCP : 개방 폐쇄 원칙 (Open/Closed Principle)
- LSP : 리스코프 치환 원칙 (Liskov Substitution Principle)
- ISP : 인터페이스 분리 원칙 (Interface Segregation Principle)
- DIP : 의존성 역전 원칙 (Dependency Inversion Principle)
위 5개의 원칙 중 오늘은 2가지 OCP와 DIP에 대해 알아보겠습니다.
1. OCP
OCP(개방 폐쇄 원칙, Open/Closed Principle)를 쉽게 정리하면 다음과 같습니다.
어떤 객체의 기능이 수정될 때, 그 객체를 사용하는 객체는 수정되어선 안된다.
여러 정의를 보면서 제 나름대로 한 줄 정리를 해본 결과입니다.
어떤 객체의 기능이 변경될 때, 그 객체를 사용하는 코드가 변경되어선 안된다 라는 원칙입니다.
객체지향의 관점에서 보면 어찌보면 당연한 얘기 일 수 있지만, 실제로 구현하다 보면 OCP를 위반하는 경우가 있습니다.
아래 예제 코드를 보겠습니다.
public class MemberService{
private MemberRepository memberRepository = new MemoryMemberRepository();
}
MemberService 는 MemberRepository interface를 멤버로 참조하고 있는데, 그 구현체가 MemoryMemberRepository 라는 의미의 코드입니다.
이 코드는 현재 OCP를 위반하고 있습니다. 왜일까요?
만약, 이 상태에서 MemberRepository의 기능이 변경되어 구현체가 MemoryMemberRepository에서 JDBCMemberRepository로 변경된다고 가정해봅시다.
그렇다면, MemberService의 코드는 아래처럼 변경되어야 합니다.
public class MemberService{
// 변경 전
// private MemberRepository memberRepository = new MemoryMemberRepository();
// 변경 후
private MemberRepository memberRepository = new JDBCMemberRepository();
}
분명 변경된 것은 MemberRepository의 구현체만 변경됐는데, 이를 참조한다는 이유로 MemberService의 코드가 수정되어 버린 것 입니다.
"아니, 자바 코딩하면 이정돈 당연히 있는 일 아니야?" 라고 생각할 수 있습니다.
(저처럼)
하지만, 좋은 객체지향 설계는 위 처럼
구현체가 변경되었다고, 그 interface를 사용하는 객체까지 변경하는 것은 피하라
라고 말합니다. 이것이 바로 OCP 입니다.
2. DIP
DIP(의존성 역전 원칙, Dependency Inversion Principle)를 쉽게 설명하자면 아래와 같습니다.
객체 참조 시, 구현체를 참조하지말고 인터페이스를 참조하세요.
즉, 객체 참조 시, 해당 객체 내에서 직접 구현체를 생성하지 말라는 의미입니다.
아래 코드를 보겠습니다.
public class MemberService{
private MemberRepository memberRepository = new MemoryMemberRepository();
public void save(Member member){
memberRepository.save(member);
}
}
MemberService는 현재 MemberRepository 라는 인터페이스를 참조하고 있습니다. 하지만 문제는 그 인터페이스의 구현체 또한 같이 참조되고 있는 것입니다. DIP를 준수하기 위해 구현체를 제거하면 어떻게 될까요?
public class MemberService{
private MemberRepository memberRepository;
public void save(Member member){
memberRepository.save(member);
}
}
문제가 있던 MemoryMemberRepository 구현체를 선언하는 부분을 제거한 모습입니다.
이 코드는 과연 정상일까요? 아니죠. 컴파일에러는 안날지언정, 실행하면 바로 NullPointerException 예외가 발생할 것입니다.
그럼 이 문제를 어떻게 해결하면 좋을까요?
2. OCP / DIP 위반 사례
아래 코드는 앞서 설명한 OCP와 DIP를 모두 위반한 코드 입니다.
public class MemberService{
private MemberRepository memberRepository = new MemoryMemberRepository();
public void save(Member member){
memberRepository.save(member);
}
}
위반한 내용은 다음과 같습니다.
- MemberRepository의 구현체가 MemoryMemberRepository 에서 JDBCMemberRepository 로 변경되는 경우, 구현체만 변경됐는데도 MemberService의 코드가 수정되어야 한다. (OCP 위반)
- MemberService 내에서 MemberRepository의 구현체인 MemoryMemberRepository를 선언해서 사용하고 있다.(DIP 위반)
두 위반사항을 고치기 위한 방법은 MemberService 에서 MemoryMemberRepository를 제거하는 것 입니다. 적용해보면 아래와 같습니다.
public class MemberService{
private MemberRepository memberRepository;
public void save(Member member){
memberRepository.save(member);
}
}
아까 DIP 설명할 때 봤던 NullPointerException을 야기하는 코드가 되었네요. 여기서 예외없이 정상 동작하는 객체를 만들기 위해서는 어떤 개선 방법이 있을까요?
3. 개선사항 1 - 의존관계 주입(DI)
위 코드를 개선하는 방법은
바로, 생성자를 통해 memberRepository를 초기화 해주는 것 입니다.
public class MemberService{
private MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
public void save(Member member){
memberRepository.save(member);
}
}
이렇게 되면 memberRepository는 초기화가 보장되어 NullPointerException으로 부터 자유로워 질 수 있습니다. 하지만 위 코드에는 2가지의 전제조건이 존재합니다.
- MemberService 객체를 사용하는 쪽에서 memberRepository를 생성자 파라미터로 넘겨줘야 한다.
- 이 때, memberRepository 는 interface가 아닌 구현체 class 여야 한다.
위 2가지를 해결해보기 위해 memberService를 사용하는 main메서드를 만들어보겠습니다.
public class MemberApplication{
public static void main(String[] args){
MemberService memberService = new MemberService(new MemoryMemberRepository());
Member member = new Member(1L, "memberA");
memberService.save(member);
}
}
memberService의 입장에서 보면, main 메서드에서 MemoryMemberRepository를 생성해서 생성자의 인자로 넘겨주었으니, OCP와 DIP를 모두 지키는 방향으로 개선된 것 같네요.
이렇게, 참조하는 객체를 객체 내에서 생성해서 사용하는 것이 아니라, 외부에서 생성해서 넣어주는(주입해주는) 것을 의존관계 주입(DI, Dependency Injection) 이라고 부릅니다.
하지만, MemberApplication 객체는 여전히 OCP를 위반한 모습입니다.
그 이유는 바로 MemoryMemberRepository 구현체를 직접 생성해서 MemberService의 인자로 넘겨주기 때문이죠.
이러면 마찬가지로 MemoryMemberRepository 가 JDBCMemberRepository로 구현체 변경이 발생한다면, MemberApplication 객체의 수정은 피할 수 없습니다.
그럼 이 문제는 어떻게 해결할 수 있을까요?
4. 개선사항 2 - 관심사 분리
현재 문제점은 MemberApplication이 MemoryMemberRepository 라는 구현체를 선택하고, MemberService에 주입해준다는 것입니다.
MemberApplication이 MemberService에서 사용될 Repository가 MemoryMemberRepository 인지 JDBCMemberRepository 인지 알 필요가 있을까요?
이건 MemberApplication을 넘어 MemberService 도 알 필요가 없습니다. 왜일까요?
MemberService 코드를 한 번 보시죠.
public class MemberService{
private MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
public void save(Member member){
memberRepository.save(member);
}
}
memberService는 분명 save 메서드를 통해 member를 저장하지만, 실제로 member를 저장하는 행위를 하는 객체는 memberService 가 아닌 memberRepository 입니다.
즉, memberService는 memberRepository 가 저장을 어떻게 하는지는 알 필요가 없고, 저장만 되면 그만 인 것입니다.
그래서 MemberService 가 memberRepository가 뭔지 알 필요가 없는 것이고, 덩달아 MemberApplication도 알 필요가 없는 것입니다.
그렇다면 이쯤에서 MemberRepository interface가 어떤 구현체를 사용할 것인가? 를 관리해주는 객체가 필요합니다.
그 객체를 만들어보면 아래처럼 만들 수 있습니다.
public class AppConfig{
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
AppConfig 는 memberRepository 메서드를 통해 실제 구현체인 MemoryMemberRepository를 반환해주는 역할을 합니다. 그럼 AppConfig 를 MemberApplication 에 적용해보면 어떻게 될까요?
public class MemberApplication{
public static void main(String[] args){
AppConfig appConfig = new AppConfig();
MemberService memberService = new MemberService(appConfig.memberRepository());
Member member = new Member(1L, "memberA");
memberService.save(member);
}
}
memberService에 필요한 memberRepository를 더 이상 MemberApplication이 알 필요없이, AppConfig가 구현체를 관리하는 형태로 변경되었습니다!
이렇게, MemberApplication은 기능에 초점을 맞추고, 실제 구현체를 주입해주는 것은 AppConfig가 모두 관리하게 하는 것처럼 객체 간의 역할을 분리하는 것을 관심사를 분리한다. 라고 표현합니다.
5. 스프링으로 바꾸기
위에 개선사항으로 언급했던 의존관계 주입(DI)은 실제 스프링이 동작하는 방식입니다.
스프링은 Bean 이라고 불리는 객체를 관리하면서, 객체 간의 의존관계를 모두 주입해줍니다.
사실 여태까지 개선했던 코드는 모두 스프링에서 동작해주게 됩니다.
그럼 기존 코드를 스프링으로 변경하면 어떻게 될까요?
public class MemberApplication{
public static void main(String[] args){
// 1
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 2
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
MemberService memberService = new MemberService(memberRepository);
Member member = new Member(1L, "memberA");
memberService.save(member);
}
}
@Configuration // 3
public class AppConfig{
@Bean // 4
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
1. ApplicationContext
스프링은 이 ApplicationContext로 부터 시작하게 된다. 그래서 이 객체를 스프링 컨테이너 라고 부른다.
구현체로 AnnotationConfigApplicationContext를 사용하게 되면, @Configuration annotation을 가지는 class를 인자로 받아서 해당 class에 등록된 Bean 정보를 스프링이 관리하게 된다.
2. ac.getBean
스프링에 등록된 빈을 조회하는 기능이다. 첫 번째 인자로 빈의 이름을 넣어주고, 두 번째 인자로 해당 빈의 객체Type을 넘겨주면 실제 Bean 객체를 조회하게 된다. 통상적으로 Bean의 이름은 @Bean으로 선언된 메서드 이름이 된다.
3. @Configuration
해당 class 는 스프링 컨테이너에 대한 설정정보라는 의미의 annotation 이다.
4. @Bean
스프링이 관리하는 기본 단위 를 의미하며, @Bean으로 등록된 메서드이름으로 return 되는 객체가 매핑되어 스프링에 등록되고 관리된다.
6. 왜 스프링 인가?
앞에서 OCP/DIP를 위반한 코드를 쭉 수정해보며, 마지막에 스프링까지 적용시켜 보았다.
여기서 한 가지 궁금증이 생긴다.
"ApplicationContext, @Bean, @Configuration 등 할 일이 엄청 많아지는데 왜 굳이 이렇게까지 하지?"
그 이유는 스프링 컨테이너가 객체를 관리하게 되면, 기능적으로 엄청난 이점을 가져올 수 있어서 이다.
(자세히 어떤 이점을 가져오는지는 앞으로 스프링을 공부하면서 지속적으로 포스팅 할 예정입니다.)
현 단계에서 생각해볼 수 있는 설계관점에서의 장점이라면,
- 기능이 변경되어도, 그 기능을 사용하는 코드는 변경할 필요가 없다.
- interface의 구현체가 뭐가 쓰이는지 한 눈에 볼 수 있다.
- 객체 간 구현체의 의존이 없으니, 객체의 재사용성이 증가한다.
이런 어떻게 보면 이론적인(?) 장점도 피부에 와닿으려면 그만큼 본질을 이해하고, 그걸 발휘할 실력이 뒷받침 되어야 하나보다.
(갈 길이 멀다)
7. 마치며
토이 프로젝트를 진행하면서 스프링에 대한 기초가 부족하다고 느껴져 시간을 조금 할애해 스프링의 기초를 매일 공부하고, 이렇게 블로그에 정리해보려 한다.
열심히 공들여 쌓아 올린 탑은 쉽게 무너지지 않으니, 좀 걸리더라도 탄탄히 쌓아보자...!
해당 게시글은 인프런에서 김영한님의 스프링 핵심 원리 강의를 공부하고 작성한 내용입니다.
인프런 - 스프링 핵심 원리(김영한) 강의 바로가기
긴 글 읽어주셔셔 감사합니다.
'Spring' 카테고리의 다른 글
[Spring] Spring AOP - 관심사 분리(부가기능과 핵심기능) (0) | 2024.02.26 |
---|---|
[Design Pattern] 프록시 패턴 & 데코레이터 패턴 (0) | 2024.02.15 |
[Design Pattern] 템플릿/콜백 패턴(Template/Callback Pattern) (0) | 2023.12.08 |
[Spring Rest Docs] 테스트 코드를 통한 API 문서 만들기 (0) | 2023.10.20 |
[ArgumentResolver] 로그인 여부를 ArgumentResolver로 처리하기 (0) | 2023.04.25 |
댓글