*인프런 김영한 강사님의 강의를 수강하고 정리한 것입니다*
1. 객체지향 설계와 스프링
1)스프링 프레임 워크, 스프링 부트
*스프링 프레임 워크(가장 중요)
-핵심 기술(모든 라이브러리들이 활용하는 스프링의 가장 핵심) : 스프링 DI 컨테이너, AOP, 이벤트, 기타 🟡
-웹 기술 ,데이터 접근 기술(DB에 접근),기술 통합 ,테스트, 언어 : 코틀린, 그루비
=> 이를 통합한 것을 스프링 프레임 워크라고 한다.
최근에는 스프링 프레임 워크를 편리하게 사용할 수 있게 하는 스프링 부트 많이 사용
*스프링 부트
-스프링 편리하게 사용할 수 있게 지원, 최근에는 실무에서 스프링 부트 기본으로 사용.
-단독 실행 가능한 스프링 애플리케이션 쉽게 생성, Tomcat 같은 웹서버를 내장해서 별도의 웹 서버 설치 필요 X -> 스프링 부트가 build와 서버 띄우는거까지 가능.
-손쉬운 빌드 구성을 위한 starter 종속성 제공 -> 필요한 라이브러리를 가지고 올 때 스타터가 필요한 여러 라이브러리 가져오게 구성. 편리함
-관례에 의한 간결한 설정 -> 필요할 때만 custom할 수 있게, 예전과 달리 매우 편해짐.
+)스프링 부트는 스프링 프레임워크와 별도로 사용X. 스프링 부트는 프레임 워크를 편리하게 사용할 수 있게 중간에서 기능 제공하는 역할,
2)다형성
- 역할(인터페이스)과 구현(인터페이스 구현한 객체)로 구분
=>역할과 구현으로 분리 시 세상이 단순, 유연해지고 변경도 편리해진다. 확장가능한 설계, 클라이언트에 영향 주지 않고 변경 가능.
=>장점: 클라이언트는 대상의 역할(인터페이스)만 알면 되고 내부 구조 변경에 영향을 받지 않는다.
*자바 언어의 다형성을 활용 : 역할과 구현 = 인터페이스와 인터페이스를 구현한 클래스(구현 객체).
객체 설계 시 역할과 구현을 명확하게 분리,
"구현보다 인터페이스가 먼저" - 역할을 먼저 부여하고, 역할 수행하는 구현 객체 만들기.
*자바 언어의 다형성 - 오버라이딩(메소드 재정의, 자식 클래스의 필요에 따라 상속된 메소드를 다시 정의)된 메소드가 실행. 다형성으로 구현한 객체를 실행 시점에 유연하게 변경 가능.
*다형성의 본질 - 클라이언트를 변경하지 않고 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경.
*역할과 구분 분리의 한계- 인터페이스가 변하면 클라이언트, 서버에 큰 변경 필요(ex-자동차를 비행기로 변경 시 다른 면허 필요) =>인터페이스를 안정적으로, 변화가 적게 잘 설계하는게 진짜 중요,
3)SOLID, 좋은 객체 지향 설계의 5가지 원칙
(1)SRP 단일 책임 원칙(Single responsibility principle):
-한 클래스는 하나의 책임만. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것(변경 시 다른 여러가지 많이 변경할 필요가 없다면 원칙을 잘 따른것).
(2) OCP 개방-폐쇄 원칙(Open/closed principle): ✨
-소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다(확장 시 코드 변경이 필요할텐데 어떻게?)
-다형성을 활용(역할과 구현 분리) -> 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현.
*OCP 개방-폐쇄 원칙 문제점: MemberService 클라이언트가 구현 클래스를 직접 선택. ->구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다.변경에 닫혀있지 않아 OCP가 지켜지지 않음
해결=>객체를 생성하고, 연관관계를 맺어주는 별도의 조립, 설정자가 필요. 스프링이 해줌
(3)LSP 리스코프 치환 원칙(Liskov substitution principle):
-프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
-(ex)엑셀 인터페이스가 있을 때 엑셀을 밟으면 +10이 된다는 기능이 있다면, 엑셀 구현 객체는 엑셀 밟으면 +10 되게 구현해야 함. 만약 -10되게 하면 컴파일되지만 엑셀 밟으면 +10되야 한다는 규약에 어긋남(=>LSP원칙에 어긋남).
-다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 함. 이걸 지켜야 구현체를 믿고 사용 가능.
(4)ISP 인터페이스 분리 원칙(Interface segregation principle):
-특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
-(ex)자동차라는 인터페이스 하나보다 운전/정비 인터페이스 두 개로 분리하는게 나음. 사용자 클라이언트를 운전자/정비사 클라이언트로 분리 가능. 분리하면 정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않음
-인터페이스가 명확해지고, 대체 가능성이 높아짐.
(5)DIP 의존관계 역전 원칙(Dependency inversion principle): ✨
-프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.”,
->쉽게 이야기해서 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻(인터페이스만 알고 구현체는 몰라야 함) 역할에 의존하게 해야 한다는 것으로 인터페이스에 의존해야 유연하게 구현체를 변경 가능.
-그러나, 예제는 인터페이스에 의존하지만(=알고 있으면), 구현 클래스도 동시에 의존. -> 구현체인를 클래스 내에서 할당받아 사용 중. 이는 구현체도 의존한 것. . =>DIP 위반
2. 예제에 객체지향 원리 적용
1)관심사의 분리
:기존 예제는 DIP, OCP위반. 애플리케이션도 구현체들은 본인의 역할만 수행하고, 인터페이스에 어떤 구현체가 할당될지는 AppConfig가 해줘야 함.
*AppConfig - 애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스.
-기존에는 구현체 내에서 직접 설정해 줬지만 이젠 AppConfig에서 memeberService생성. 생성자를 통해서 구현체 설정.
구현체에 대한 것은 전혀 모르고 있고, 오직 인터페이스에 대한 것만. 추상화에만 의존 <=DIP를 지키는 것. "생성자 주입".
이제 각 구현체들은 담당 기능을 실행하는 책임만 지면 된다. OrderServiceImpl 은 기능을 실행하는 책임만 지면 된다.
=>DIP와 단일 책임 원칙을 지킨 것.
2)AppConfig 리팩터링
:그러나 코드를 보면 역할에 따른 구현이 잘 안보임.
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl( memberRepository(), discountPolicy());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
리팩터링 후 코드를 보면 역할과 구현을 명확하게 분리,
각 인터페이스가 드러나 있어 메소드명을 보는 순간 역할이 드러남. 어떤 구현체를 쓰고, 구현체 변경 시 어디 코드만 수정해야 할지(+전체 구성) 한눈에 알 수 있음. +)new로 생성하는 중복이 제거됨.
3)의존관계 주입
(1)정적: 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 O. 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석 가능. 하지만 실제로 어떤 구현체(객체)가 주입될지는 알 수 없다.
(2)동적: 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다
-애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다
-클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다
-정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 O.
3.스프링 빈
1)스프링 컨테이너
:생성과정
(1)스프링 컨테이너 생성
new AnnotationConfigApplicationContext로 appConfig의 정보를 줌.
->스프링 컨테이너 생성(키는 빈의 이름이 되고 값은 빈의 객체가 됨) //스프링 컨테이너 생성 시 구성 정보를 지정해줘야 하는데 그게 바로 AppConfig(구성 정보)를 파라미터로 넣어준 것. 이정보를 보고 객체 생성
(2)스프링 빈 등록
구성 정보를 넘기고 @Bean이 붙은 모든 것을 다 호출하고, 메소드 이름을 키 값으로 하여 빈 이름으로 저장, 반환 객체를 빈 객체로 등록. +)빈 이름은 직접 부여도 가능은 한데 빈이름은 항상 다른 이름을 부여해야 함.
(3)스프링 빈 의존관계 설정-준비
(4)의존 관계 설정 완료
의존관계를 주입(DI). 동적인 의존 관계를 스프링이 다 연결. 객체의 참조값들이 다 연결.
자바 코드를 호출하는 것 같지만, 차이가 O, 차이는 뒤에 싱글톤 컨테이너에서 설명.
스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져 있음(스프링 빈 객체 생성하고 엮어줌.)
그런데 이렇게 자바 코드로 스프링 빈을 등록 시 생성자를 호출하면서 의존관계 주입도 한번에 처리.
=>컨테이너에 빈이 잘 등록되었는지 확인
2)스프링 빈 조회
:빈 조회 시 상속 관계가 있다면? 부모 조회 시 자식들은 같이 조회됨,
DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class); 이렇게 빈 이름 지정, or
DiscountPolicy rateDiscountPolicy = ac.getBean(rateDiscountPolicy.class); 이렇게 특정 하위 타입으로도 지정 가능. 구현 객체로 바로 조회. 구현에 의존하고 있기 때문에 좋지 못한 코드.
-------------------------------------------------------
자바의 최상위 객체인 Object로 조회 시 모든 스프링 빈 조회 가능.
@Test
@DisplayName("부모 타입으로 모두 조회하기 - Object")
void findAllBeanByObjectType() {
Map beansOfType = ac.getBeansOfType(Object.class); f
or (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " value=" + beansOfType.get(key)); }
} //스프링 내부에서 사용하는 Bean까지 다 조회됨.
3)빈팩토리
:BeanFactory는 스프링 컨테이너의 최상위 인터페이스로 스프링 빈을 관리하고 조회하는 역할. getBean()을 제공하고 우리가 사용한 많은 기능을 BeanFactory가 제공하는 기능.
ApplicationContext는 BeanFactory 기능 모두 상속받아 제공, BeanFactory 인터페이스를 상속받은 ApplicationContext 인터페이스.
정리:ApplicationContext는 BeanFactory의 기능을 상속받아 빈 관리기능 + 편리한 부가 기능을 제공.
BeanFactory를 직접 사용할 일은 거의 X. 부가기능이 포함된 ApplicationContext를 사용.
BeanFactory나 ApplicationContext를 스프링 컨테이너라 한다.
4.싱글톤 컨테이너
-스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청마다 객체 새로 생성 ->초당 n개의 객체가 생성되고 소멸되는 것이기에 메모리 낭비가 심함
-해결방안: 싱글톤 패턴 : 해당 객체가 1개만 생성되고, 공유하도록 설계 ~>효율적
1)싱글톤 패턴
-클래스 인스턴스가 딱 1개만 생성되게 보장하는 디자인 패턴
방법:객체 2개 이상 생성 제한.->private 생성자를 사용해 외부에서 임의로 new 키워드 사용 못하게 해야함. +
->두번 요청해도 같은 객체 반환. 하나 만들어두고 반환하는 형태.
-appConfig를 싱글톤으로 수정 필요 ?
스프링 컨테이너 사용하면 알아서 객체를 싱글톤으로 관리해줌. 요청 많아도 만들어놓은 1개의 객체 재활용(공유)함.
---------------------------------------------------------------------
하지만 싱글톤 패턴은 수많은 문제가 있음.
싱글톤 패턴을 구현하는 코드 자체가 많음, 의존 관계 상 클라이언트가 구체 클래스에 의존. getInstance로 객체 가져오는 것이기에 -> DIP위반, OCP 위반 가능성 높음, 테스트 하기가 어려움.인스터 미리 받아 설정이 먼저 끝나기에 유연하게 테스트하기 어려움.(유연성이 떨어짐), 내부 속성을 변경하거나 초기화 어렵다, private 생성자->자식 생성하기 어려움.
->결론적으로 유연성이 떨어져 DI 같은 것 적용 힘듬.
=>>스프링이 싱글톤 패턴 문제, 단점 다 해결하고, 싱글톤이 적용된 싱글톤 컨테이너 제공.
2)싱글톤 컨테이너
- 스프링은 싱글톤 패턴의 단점 해결하면서도 객체 인스턴스를 싱글톤으로 관리한다. ->@annotation 사용해 코드 깔끔. Solid위반 X, 코드 유연, private 생성자 X
- 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리. 객체가 하나만 생성.
- 스프링 빈이 바로 싱글톤으로 관리되는 빈 ->@Bean을 호출해 빈 객체를 미리 등록해(객체 미리 생성해) 관리.
- 스프링 컨테이너는 싱글톤 컨테이너의 역할을 하고, 싱글톤 객체를 생성/관리하는 기능을 싱글톤 레지스트리라고 함.
- 위에서 사용한 싱글톤 패턴 코드가 하나도 없음.
3)싱글톤 방식 주의할 점
:여러 클라이언트가 공유해서 사용하는 객체이기에 싱글톤 객체는 상태를 유지하게 설계하면 안됨. 무상태로 설계해야 함.
- 특정 클라이언트에 의존적인 필드가 있으면 안된다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
- 가급적 읽기만 가능해야 한다.
- 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있음.
'스프링 > 핵심원리-기본' 카테고리의 다른 글
싱글톤 컨테이너 (0) | 2022.10.02 |
---|---|
웹 애플리케이션과 싱글톤 (0) | 2022.09.21 |
스프링 객체지향 설계 적용 (2) (0) | 2022.09.18 |
스프링에 객체지향 원리 적용 (0) | 2022.09.18 |
스프링 예제 (0) | 2022.09.06 |