[Spring] OOP원칙과 DI, 그리고 config 파일(간략)
인프런 김영한님의 Spring 핵심 강의를 복습하는 글입니다
강의를 수강함으로서 객체지향개념을 직관적으로 경험 할 수 있었습니다.
- 객체지향 설계 원칙을 준수함으로서 [ 유연한 설계 ] 가 가능하다.
간단하게, Spring을 사용하지 않았을 때, 설계를 구현하는 과정과 , 이 때 어떤 객체지향 설계원칙을 어기게 되는지 살펴보자.
이 때 의존성 주입의 필요성을 느끼게 되는데 이를 위한 config 파일을 간단하게 살펴본 후 ,
Spring을 도입할 경우에는 어떤 차이가 있는지 살펴본다.
Spring을 사용하지 않는다면?
- Spring과 관련된 거 하나 없이 , 순수한 JAVA코드로 이를 작성할 수 있다.
회원 서비스
설계자체를 보면, 상속 보다도, composite을 활용하여 매우 잘 설계된 것 처럼 보인다.
회원 도메인
@Getter @Setter
public class Member {
private Long id;
private String name;
private Grade grade;
public Member(Long id, String name, Grade grade) {
this.id = id;
this.name = name;
this.grade = grade;
}
회원 레포지토리
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
- 이는 흡사 JPA repository interface 모습
public class MemoryMemberRepository implements MemberRepository {
..구현 어쩌고
}
회원 서비스
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
....구현 어쩌고
}
위 설계의 문제점
현재 상황
- 마치 💥 설계만 보면, 다형성을 활용 -> 인터페이스와 구현체를 잘 분리해 놓은 것 같다.
- DIP 준수?
✌ DIP(Dependency inversion principle) : 의존관계 역전 원칙
- 사용자는 [ 추상화에 의존 ] 해야 한다. [ 구체 화에 의존하면 ❌ ] --> interface에 의존해야 한다
- 즉, 역할에 의존하여 사용해야지, 구현체를 변경하는 것이 가능하다. 사용자가 구현체에 의존한다면 변경이 어려워진다.현재 상황 : [ Class의 의존관계 ] 를 분석 --> interface 뿐만 아니라 구현체에도 의존하고 있다.
===> MemberRepository의 구현체를 다른 것으로 바꿔끼우기 위해 MemberServiceImpl 코드에 직접 수정을 하게 된다.public class MemberServiceImpl implements MemberService { private final MemberRepository memberRepository = new MemoryMemberRepository(); ....구현 어쩌고 }
- OCP 준수 ?
✌ OCP(Open/closed principle) : 개방-폐쇄 원칙 : SW 요소는 [ 확장에는 열려 있으나, 변경에는 닫혀 있어야 한다 ]
- 다형성을 잘 이용하면 가능
- "역할"과 "구현"의 분리를 한다면 가능.MemberRepository의 구현체를 확장 시 문제 발생(위에서 본 것 처럼 , 코드에 변화가 생긴다) : MemberRepostiory의 하위 클래스로 MemoryMemberRepository 뿐만 아닌, DbMemberRepository로 확장할 경우 -> Client 코드 자체에 영향이 가는 것. -> OCP 위반
🚀 Spring을 사용한다면?
이를 Spring의 Bean 주입을 사용하는 코드로 변경한다면 어떻게 될까 ?
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
....구현 어쩌고
}
- 구현체 (MemoryMemberRepository) 에 의존하지 않고, interface 에 의존하게 된다.
- 즉, 누군가가 Client인 MemberServiceImpl에 MemberRepository의 구현 객체를 대신 생성하고 주입 해 주면 된다.
AppConfig의 등장
Spring Bean 도입 하지 않을 것임. 여전히 Spring을 사용하지 않은 상황에서 개선해가는 과정을 설명한다.
- 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, [ 구현 객체를 생성하고, 연결하는 책임 ]을 가지는 별도의 [ 설정 클래스 ]를 만든다.
- app 전반의 구성을 책임지는 설정 클래스는, 패키지 바로 밑에 생성 .
DI : 의존성 주입
DI (Dependency Injection) : 의존성 주입
- Client입장에서, 의존관계를 마치 외부에서 주입해주는 것과 같다고 하여, DI라고 한다.
- Client 측에서는, 어떤 인스턴스를, 다형성을 이용하여, 주입을 받고 ,
- 별도의 클래스에서, 해당 인스턴스를 생성하고, 연결 해 준다면?
Appconfig
- Appconfig 에서, MemberService에 필요한 💥 MemberRepositry 인스턴스를 생성하여, MemberService의 생성자를 호출하며 이를 인자로 넘겨준다.
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
}
}
MemberServiceImpl
- 생성자 주입 , 생성자를 호출 시, 인자로 MemberRepository 인스턴스를 전달 받는다 .
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
test code 예시
public class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach()
{
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
...
}
public class OrderServiceTest {
MemberService memberService ;
OrderService orderService ;
@BeforeEach
public void beforeEach()
{
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
....
}
AppConfig의 효과
- 구현체를 밖(이 경우, AppConfig)에서 생성하여 넣어준다.
- MemberServiceImpl은, MemberRepository라는 interface에만 의존 -> 추상화에 의존(어떤 구현체가 들어올지는 알 수 없다. Polymorphism에 의해 무엇인가가 들어올 뿐) -> DIP(의존관계 역전 원칙)를 따르게 된다.
관심사가 분리 되었다.
- MemberServiceImpl은, 회원 서비스 역할에만 충실할 수 있게 되었다. 따로 MemberRepository의 인스턴스를 생성하던 것을 하지 않아도 된다.
DI : 의존성 주입
--> 외부에서 생성한 인스턴스를 Cleint가 주입받아 사용한다.
AppConfig 의 역할
- [ 설정 정보 ] 의 역할을 한다.
- 따라서, [ 어떤 역할 ] 에 [ 어떤 구현체를 담고 있다] 라는 것이 한 눈에 보일 수 있는게 좋다.
- MemberRepository의 구현체를 DB구현체로 바꿀 경우, 여기서만 바꿔주면 된다.
- 중복을 제거해 주도록 해야 한다.
클래스 다이어그램
Spring Bean 도입 하지 않을 것임. 여전히 Spring을 사용하지 않은 상황에서 개선해가는 과정을 설명한다.
- app 전반의 구성을 책임지는 설정 클래스는, 패키지 바로 밑에 생성 .
DI (Dependency Injection) : 의존성 주입
- Client입장에서, 의존관계를 마치 외부에서 주입해주는 것과 같다고 하여, DI라고 한다.
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
}
}
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach()
{
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
...
}
public class OrderServiceTest {
MemberService memberService ;
OrderService orderService ;
@BeforeEach
public void beforeEach()
{
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
....
}
관심사가 분리 되었다.
- MemberServiceImpl은, 회원 서비스 역할에만 충실할 수 있게 되었다. 따로 MemberRepository의 인스턴스를 생성하던 것을 하지 않아도 된다.
DI : 의존성 주입
--> 외부에서 생성한 인스턴스를 Cleint가 주입받아 사용한다.
- 따라서, [ 어떤 역할 ] 에 [ 어떤 구현체를 담고 있다] 라는 것이 한 눈에 보일 수 있는게 좋다.
- MemberRepository의 구현체를 DB구현체로 바꿀 경우, 여기서만 바꿔주면 된다.
- 중복을 제거해 주도록 해야 한다.
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
public DiscountPolicy discountPolicy(){
return new FixDiscountPolicy();
}
}
- 이렇게 해 줌으로서, memberservice역할, memberRepository역할, orderservice역할,discountPolicy역할 까지 다 드러남 -> 한 눈에 보인다
- 현재 나의 application에서는 memberService는 MemberServiceImpl을 사용할 거야.
- MemberRepository는 MemoryMemberRepository를 할 거야. → 나중에 DB로 바뀐다면, AppConfig쪽에서 return new MemoryMemberRepository(); 이 코드만 바꿔주면 되는것.
- discountPolicy또한 나중에 그렇게 변경해주면 되는 것.
IoC와 DI
IoC(Inversion of control)
- Framework를 사용하기 전!! 에는, 개발자가 직접 필요한 객체를 생성, 호출 , 그 안에서 또 다른 객체를 생성, 호출 -> 직접 컨트롤 , 제어
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();//이런식
.....
}
IoC : 개발자가 아닌, [ Framework가 대신 "생성" && 호출 ]
- AppConfig가 등장한 이후 [ 구현 객체는 자신의 로직을 실행하는 역할만 담당 ] 하게 됨.
- AppConfig가 [ 프로그램에 대한 제어 흐름 권한 ] 을 갖게 됨. -> [ AppConfig와 같은 외부 ]에서 [ 흐름을 관리 ]하게 되어 -> 제어의 역전이 일어남 .
프레임워크? 라이브러리?
- 작성하는 코드에서 [ 내가 직접 ] 제어의 흐름을 담당 -> 라이브러리를 사용하는 것
- 나는 로직만을 작성 했고, 실행과 제어를 [ 외부에서 ] 담당 -> Framework
- FrameWork는, [ 자신의 lifeCycle ]이 존재함
- 예를 들어 Junit framework
@BeforeEach
를 먼저 실행 -> @Test
-> @AfterEach
이런 라이프 사이클 속에서 실행
DI (Dependency Injection)
의존관계는
- 정적인 클래스 의존관계 ( 클래스 다이어그램 )
- 동적 객체(인스턴스) 의존 관계 ( 객체 다이어그램)
를 분리해서 생각해야 한다.
IoC 컨테이너, DI 컨테이너
- AppConfig처럼 [ 객체를 생성,관리 ] 하면서 [ 의존관계를 연결 ] 해주는, [ 제어의 역전을 일으키는 ] 것을 ---> "IoC 컨테이너" == "DI 컨테이너" 라고 한다
[ Spring도 이런 DI container 역할 ] 을 해 주는 것
Spring으로 넘어오자
먼저
- AppConfig -> Spring기반으로 변경
Appcofing
- 설정 정보에
@Configuration
어노테이션 붙이기
- 각 method에는
@Bean
을 붙이기
- method가 리턴하는 인스턴스들이 [ Spring Container에 Spring Bean으로 자동등록 ] 된다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}
🚀 return type을 interface타입으로
- 이걸 보고 "역할"을 알 수 있는 것이라 생각하면 된다.
- [ 의존관계를 주입할 때에도 "특정한 역할" 에만 의존하는게 '] 객체지향관점에서 더 좋다.
설정정보 사용 예시
- 참고로, Test framework를 사용하지 않고, main method 를 사용하여 테스트 하는 예시다.
Spring을 사용 하기 전
- 개발자가 AppConfig를 사용하여, 필요한 객체를 직접 조회
//Test unit을 사용하지 않은 test.
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService(); // 이런식으로
//MemberService memberService = new MemberServiceImpl();
Member member = new Member(1L, "memberA", Grade.VIP);//Long type이라서 L붙여줘야함.
memberService.join(member);
//
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("findMember = " + findMember.getName());
}
}
Spring 사용 후
- Spring Container로부터 [ 이미 등록되어있는 Bean을 찾아와 ] 사용한다.
public class MemberApp {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.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("findMember = " + findMember.getName());
}
}
ApplicationContext
- Spring은 모든게 ApplicationContext라는 것으로 시작 (???)
- 모든 객체들(Spring Bean) 을 관리해 준다.
AnnotationConfigApplicationContext(AppConfig.class);
- @Configuration 어노테이션을 class-level에 가진 AppConfig.class를 인자로 전달
- Appconfig 에 있는 설정정보를 갖고, Spring이 @Bean 붙은 것들을 [ Spring Container에 객체 생성 및 관리 ]하게 된다.
ApplicationContext로부터 Bean을 찾아올 수 있다.
ac.getBean("memberService", MemberService.class);
- 이는 Spring Container에서 관리하고 있는 Bean을 가져오는 것.
- 위의 코드는 [ 찾아오려는 Bean의 이름 ] 을 적어 준 것
- Bean의 등록은 [ 기본적으로 ][ config파일에서 Bean을 생성하여 리턴하는 method이름 ] 으로 등록이 되어있다.
- 이런식으로 [ 직접 Bean의 이름을 지정 ]도 가능
@Bean(name="MemberSerViceBean")
싱글톤 인스턴스 Bean
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();//이런식
.....
}
IoC : 개발자가 아닌, [ Framework가 대신 "생성" && 호출 ]
- FrameWork는, [ 자신의 lifeCycle ]이 존재함
- 예를 들어 Junit framework
@BeforeEach
를 먼저 실행 ->@Test
->@AfterEach
이런 라이프 사이클 속에서 실행
의존관계는
- 정적인 클래스 의존관계 ( 클래스 다이어그램 )
- 동적 객체(인스턴스) 의존 관계 ( 객체 다이어그램)
를 분리해서 생각해야 한다.
[ Spring도 이런 DI container 역할 ] 을 해 주는 것
먼저
- AppConfig -> Spring기반으로 변경
Appcofing
- 설정 정보에
@Configuration
어노테이션 붙이기- 각 method에는
@Bean
을 붙이기
- method가 리턴하는 인스턴스들이 [ Spring Container에 Spring Bean으로 자동등록 ] 된다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}
🚀 return type을 interface타입으로
- 이걸 보고 "역할"을 알 수 있는 것이라 생각하면 된다.
- [ 의존관계를 주입할 때에도 "특정한 역할" 에만 의존하는게 '] 객체지향관점에서 더 좋다.
설정정보 사용 예시
- 참고로, Test framework를 사용하지 않고, main method 를 사용하여 테스트 하는 예시다.
Spring을 사용 하기 전
- 개발자가 AppConfig를 사용하여, 필요한 객체를 직접 조회
//Test unit을 사용하지 않은 test. public class MemberApp { public static void main(String[] args) { AppConfig appConfig = new AppConfig(); MemberService memberService = appConfig.memberService(); // 이런식으로 //MemberService memberService = new MemberServiceImpl(); Member member = new Member(1L, "memberA", Grade.VIP);//Long type이라서 L붙여줘야함. memberService.join(member); // Member findMember = memberService.findMember(1L); System.out.println("new member = " + member.getName()); System.out.println("findMember = " + findMember.getName()); } }
Spring 사용 후
- Spring Container로부터 [ 이미 등록되어있는 Bean을 찾아와 ] 사용한다.
public class MemberApp { public static void main(String[] args) { ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); MemberService memberService = ac.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("findMember = " + findMember.getName()); } }
ApplicationContext
- Spring은 모든게 ApplicationContext라는 것으로 시작 (???)
- 모든 객체들(Spring Bean) 을 관리해 준다.
AnnotationConfigApplicationContext(AppConfig.class);
- @Configuration 어노테이션을 class-level에 가진 AppConfig.class를 인자로 전달
- Appconfig 에 있는 설정정보를 갖고, Spring이 @Bean 붙은 것들을 [ Spring Container에 객체 생성 및 관리 ]하게 된다.
ApplicationContext로부터 Bean을 찾아올 수 있다.
ac.getBean("memberService", MemberService.class);
- 이는 Spring Container에서 관리하고 있는 Bean을 가져오는 것.
- 위의 코드는 [ 찾아오려는 Bean의 이름 ] 을 적어 준 것
- Bean의 등록은 [ 기본적으로 ][ config파일에서 Bean을 생성하여 리턴하는 method이름 ] 으로 등록이 되어있다.
- 이런식으로 [ 직접 Bean의 이름을 지정 ]도 가능
@Bean(name="MemberSerViceBean")
싱글톤 인스턴스 Bean
위의 MemberApp의 main method를 실행하면 아래와 같은 결과가 나온다.
- 현재 bean은 싱글톤 scope으로 형성된다.(default) ( Bean 의 scope은 이와 다른 설정도 가능하다)
- 위의 5개의 bean은 Spring 내부적으로 필요해서 등록하는 bean, 그 밑의 bean들은 AppConfig에서 직접 등록한 Bean들이다. -> method이름으로 등록됨을 볼 수 있다.
Author And Source
이 문제에 관하여([Spring] OOP원칙과 DI, 그리고 config 파일(간략)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@ynoolee/Spring-01저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)