Ian's Archive 🏃🏻

thumbnail
스프링 핵심 원리 강의 정리 - 김영한
Spring
2022.03.01.

Spring

스프링의 가장 큰 핵심은 좋은 객체지향 어플리케이션 개발에 도움

1. 객체 지향 설계

역할(인터페이스)과 책임(구현체) 분리

  • 구현체를 실행 시점에 유연하게 변경가능
    => 클라이언트 변경 없이 서버 구현기능 변경
  • 의존관계 주입(DI), 제어의 역전(IoC)

좋은 객체지향 설계의 5가지 원칙

SRP: 단일 책임 원칙(single responsibility principle)

  • 한 클래스는 하나의 책임
  • 변경 / 변경이 있을 때 파급효과가 적어야 함

OCP: 개방-폐쇄 원칙 (Open/closed principle)

  • 역할과 책임 분리
  • 인터페이스를 구현한 클래스(구현체)로 기능 구현
  • 객체 생성, 연관관계 생성, 설정자가 필요

LSP: 리스코프 치환 원칙 (Liskov substitution principle)

  • 컴파일에 성공하는 것을 넘어서 기능까지 만족해야 한다.

ISP: 인터페이스 분리 원칙 (Interface segregation principle)

  • 특정 클라이언트를 위한 여러개의 인터페이스가 범용 인터페이스 보다 낫다

DIP: 의존관계 역전 원칙 (Dependency inversion principle)

  • 추상화에 의존해야하고 구체화에는 의존하면 안된다
  • (Interface 에만 의존해야 한다.)

관심사 분리

구현 객체에서 직접 추상화된 객체 의존을 선택하게 되면 DIP에 위반된다.
-> 구현 객체와 추상화된 객체를 연결하는 설정 클래스로 관리한다. -> AppConfig
=> 이렇게 하면 구성 영역사용 영역으로 분리하는 효과를 가진다.

2. IoC, DI, Container

제어의 역전 (IoC)

프로그램이 객체의 생성관리

DI

실행 시점(런타임)에 외부에서 구현 객체를 생성해 의존관계 설정
-> 동적으로 객체 의존관계 변경 가능

프레임워크 vs 라이브러리

  • 프레임워크 : 프로그램이 코드 제어 및 실행
  • 라이브러리 : 내가 작성한 코드가 제어의 흐름 담당

DI Container

객체 생성, 관리, 의존관계 연결

3. Container, Bean

ApplicationContext 스프링 컨테이너(Interface)

copyButtonText
AnnotationConfigApplicationContext ac =
  new AnnotationConfigApplicationContext(AppConfig.class) // 매개변수로 구성 정보 지정
  • XMl / Annotation 기반으로 스프링 컨테이너 생성 가능
  • 스프링 컨테이너를 생성할 땐 구성 정보를 지정해줘야 한다
  • Bean은 key-value형태로 저장이되고, 항상 다른 이름을 사용해야 한다.
  • MessageSource(국제화), EnviromentCapable(로컬, 개발, 운영 구분)
  • ApplicationEventPublisher(이벤트 발행, 구독 모델 지원)
  • ResourceLoader(파일, 클래스패스 리소스 조회)

bean 조회

빈을 조회할 때 부모타입으로 조회하면, 자식 타입도 함께 조회

copyButtonText
String[] beanDefinitionNames = ac.getBeanDefinitionNames(); // 모든 빈 조회

for (String beanDefinitionName : beanDefinitionNames) {
  BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
  // ROLE_APPLICATION : 사용자 정의 빈
  // ROLE_INFRASTRUCTURE : 스프링 내부 빈
  if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
    Object bean = ac.getBean(beanDefinitionName);  // 빈 이름으로 빈 객체 조회
    // ac.getBean(빈이름, 타입)                    // 빈 이름과 타입으로 빈 객체(인스턴스)를 조회
    System.out.println("name=" + beanDefinitionName +
    " object=" + bean);
  }
}

BeanFactory

  • 스프링 컨테이너 최상위 인터페이스
  • 빈 관리, 조회하는 역할
  • getBean제공

스프링 빈 설정 메타 정보 - BeanDefinition

역할 / 구현 개념 분리 => 스프링 컨테이너는 이 메타 정보를 기반으로 빈 생성

BeanDefinition 정보 (Toggle)
BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
factoryMethodName: 빈을 생성할 팩토리 메서드 지정, 예) memberService
Scope: 싱글톤(기본값)
lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부
InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명 Constructor arguments, Properties: 의존관계 주입에서 사용한다.
- (자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)

스프링이 다양한 형태의 설정 정보를 추상화해서 사용

4. Singleton Container

클래스 객체가 1개만 생기는 디자인 패턴

-> private 생성자를 사용해 임의로 new키워드 사용 방지

대부분 싱클톤패턴을 사용한다.
(HTTP Request LifeCycle, HTTP Session LifeCycle에 Bean LifeCycle 맞추는 경우같이 아주 특별한 경우에만 싱글톤을 사용하지 않음)

싱글톤 객체를 생성, 관리하는 기능을 싱글톤 레지스트리라 한다

Singleton 방식 주의점!!

여러 클라이언트가 같은 객체를 공유하기 때문에 상태를 유지하게 설계하면 안된다.

  • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
  • 값을 변경할 수 있는 필드가 있으면 안된다.
  • 가급적 읽기만 가능하게
  • 필드 대신에 자바에서 공유할 수 없는 지역변수, 파라미터, ThreadLocal 등을 사용
  • 스프링 빈 항상 **무상태(stateless)**로 설계

Configuration 어노테이션

Configuration 어노테이션은 빈의 싱글톤을 보장해준다. (Bean만 사용해도 등록은 되지만, 싱글톤x)

5. Component Scan

설정 정보가 없어도 자동으로 Bean 등록 -> Autowired 어노테이션으로 의존성 주입

Component Scan은 @Component가 붙은 모든 클래스를 빈으로 등록

  • 기본 전략 : 기본이름은 클래스 명을 사용하되 맨 앞글자만 소문자로 사용
    ex) UserImpl 클래스 -> userImpl
  • 직접 지정 : 지정하고 싶은 이름을 @Component 매개변수에 등록

ComponentScan 옵션

  • basePackages : 패키지로 시작위치 지정
  • basePackageClasses : 클래스 위치로 지정
  • includeFilters : 추가 지정
  • excludeFilters : 제외할 대상 지정

부가기능이 있는 어노테이션

@Controller : 스프링 MVC 컨트롤러
@Repository : 데이터 접근 계층 인식 - 데이터 계층 예외 -> 스프링 예외로 변환
@Configuration : 스프링 설정 정보 인식 / 싱글톤 유지
@Service : 특별한 처리 x - 개발자들이 비즈니스 로직 구분하기 위한 용도

FilterType 정보 (Toggle)

ANNOTATION: 기본값, 애노테이션 인식 ex) org.example.SomeAnnotation
ASSIGNABLETYPE: 지정한 타입과 자식 타입 인식 ex) org.example.SomeClass
ASPECTJ: AspectJ 패턴 사용
ex) org.example.._Service+
REGEX: 정규 표현식
ex) org.example.Default.

CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리 ex) org.example.MyTypeFilter

copyButtonText
  includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class)

6. 의존 관계

의존관계 주입

1. 생성자 주입

생성자 호출 시점에 한번 호출, 불변, 필수

2. 수정자 주입 (setter)

선택, 변경 가능성이 있는 의존관계에서 사용

3. 필드 주입

4. 일반 메서드 주입

옵션 처리

Bean이 없어도 동작해야 할 때

@Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 호출 안됨
@Nullable : null
Optional<> : Optional.empty

생성자 주입의 장점

  • 대부분 의존관계는 애플리케이션 종료시까지 변경이 없음.
  • 객체를 불변하게 설정해, 개발자가 실수로 객체를 변경할 경우 방지

Tip!!

  • 생성자가 하나만 있으면 @Autowired 생략 가능
  • Lombok 라이브러리 @RequiredArgsConstructor 사용 -> final붙은 필드 모아서 생성자 자동 생성
copyButtonText
@Controller
@RequestMapping("/items")
@RequiredArgsConstructor
public class BasicItemController {

  private final ItemRepository itemRepository;

  // 생략 가능
  // @Autowired
  // public BasicItemController(ItemRepository itemRepository) {
  //     this.itemRepository = itemRepository;
  // }
}

조회 대상 빈이 2개 이상일 경우 해결 방법

1) @Autowired 필드 명 매칭

Autowired 타입 매칭 시도 -> 여러빈 있을 경우 -> 필드이름 -> 파라미터 이름 순으로 추가 매칭

2) @Quilifier -> @Quilifier끼리 매칭 -> 빈 이름 매칭

copyButtonText
@Component
@Qualifier("{부르고 싶은 구분자}")
public class RateDiscountPolicy implements DiscountPolicy {
  ...
}
copyButtonText
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
  @Qualifier("{부르고 싶은 구분자}") DiscountPolicy discountPolicy) {
  this.memberRepository = memberRepository;
  this.discountPolicy = discountPolicy;
}
  1. @Qualifier 매칭
  2. 빈 이름 매칭
  3. NoSuchBeanDefinitionException 예외 발생

3) @Primary

ex) 메인 DB Connection / 서브 DB Connection -> 이런 경우 코드 가독성 향상 가능!

@Qualifier > @Primary 순으로 적용된다.

Tip!!

Qualifier는 문자라 컴파일 시 타입 체크가 안됨 -> 어노테이션을 사용해 조금 더 깔끔하게 코드 관리 가능!

모든 빈을 조회할 경우

전략 패턴 매우 쉽게 구현 가능

copyButtonText
@Test
void findAllBean() {
  ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
  DiscountService discountService = ac.getBean(DiscountService.class);

  Member member = new Member(1L, "userA", Grade.VIP);

  int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");

  assertThat(discountService).isInstanceOf(DiscountService.class);
  assertThat(discountPrice).isEqualTo(1000);
}

static class DiscountService {
  private final Map<String, DiscountPolicy> policyMap;

  public DiscountService(Map<String, DiscountPolicy> policyMap){
      this.policyMap = policyMap;
  }

  public int discount(Member member, int price, String discountCode) {
      DiscountPolicy discountPolicy = policyMap.get(discountCode);
      return discountPolicy.discount(member, price);
  }
}

자동 주입 빈 vs 수동 주입 빈

업무 로직 빈 : 비즈니스 요구 사항
기술 지원 빈 : 공통 관심사(AOP), DB연결, 로그 처리

기술 지원 빈은 수가 적고, 어플리케이션에 광범위하게 영향을 준다.
-> 수동 빈으로 명확하게 들어내는 것이 좋다.
(스프링 부트가 자동으로 등록하는 빈 제외)

스프링 다형성을 활용할 때 (전략 패턴)
-> 한번에 설정 정보를 보기 위해 수동 빈이 보기 좋을때 있다.

7. Bean LifeCycle Callback

어플리케이션을 시작 시점, 종료시점에 초기화 및 종료 작업이 필요한 경우
ex) DB Connection pool, Network Socket

빈은 간단히 보면 객체 생성 -> 의존관계 주입 을 가진다.

스프링 빈 이벤트 라이프 사이클
스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료

간단한 경우 생성자에서 초기화해주는게 좋지만, 유지보수 관점에서 보면 생성과 초기화는 분리하는게 좋다.

크게 3가지 방법으로 빈 생명주기 콜백 지원

  1. 인터페이스(InitializingBean, DisposableBean)
  2. 설정 정보에 초기화 메서드, 종료 메서드 지정
  3. @PostConstruct, @PreDestroy 어노테이션

2,3 번 방법 사용 -> 스프링 빈이 스프링 코드에 의존하지 않는다.

설정 정보에 초기화 메서드, 종료 메서드 지정

copyButtonText
@Configuration
static class LifeCycleConfig {
    @Bean(initMethod = "init", destroyMethod = "close") // 초기화, 종료 메서드 지정
    public NetworkClient networkClient() {
      NetworkClient networkClient = new NetworkClient();
      networkClient.setUrl("http://hello-spring.dev");
      return networkClient;
    }
}
  • 메서드 이름 자유롭게 줄 수 있다.
  • 코드 고칠 수 없는 외부 라이브러리 초기화, 종료 메서드 적용
  • destroyMethod 에 값을 지정해주지 않으면 close, shutdown같은 이름의 종료 메서드 추론하여 동작 => 사용하지 않으려면 destroyMethod="" 같이 빈 공백 지정해서 사용

@PostConstruct, @PreDestroy

초기화 및 종료 하고싶은 메서드에 어노테이션을 단다.

  • 외부라이브러리 사용 불가능

일반적인 경우엔 @PostConstruct, @PreDestroy사용 / 외부 라이브러리 경우 @Bean 사용

8. Bean scope

빈이 존재할 수 있는 범위

  • Singleton : 기본 스코프, 스프링 컨테이너 시작 ~ 종료까지 유지되는 스코프 (기본)
  • Prototype : 빈의 생성, 의존관계 주입까지만 관여

Prototype Bean 의존 관계 주입, 초기화까지 처리 -> 빈 관리 책임은 클라이언트에 있다.
(@PostConstruct, @PreDestroy같은 종료메서드 호출되지 않는다.) .

Singleton, Prototype 같이 쓸 경우 문제점

getBean을 통해 Bean을 주입하면 스프링 컨테이너에 종속적인 코드가 되서 새로운 빈이 생기지 않음

DL(의존관계 조회(탐색)) -> ObjectProvider (ObjectFactory 상속) .

  • 스트림 처리같은 편의 기능 제공, 별도의 라이브러리 필요x

  • 웹관련 스코프

    • request : 웹 요청 ~ 종료
    • session : 웹 세션이 생성 ~ 종료
    • application : 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
    • websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프

Tip!!

  • Spring-boot-web을 사용하면 AnnotationConfigServletWebServerApplicationContext 기반으로 애플리케이션 구동

request scope 가진 빈 -> 실제 고객 요청이 와야 생성

해결 방법 1)

ObjectProvider 사용** -> bean생성을 getObject를 실행하는 시점까지 생성 지연

해결 방법 2)

프록시 사용

copyButtonText
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)

Scope에 proxy설정을 하면 컨테이너는 CGLIB 라이브러리를 사용해 프록시 객체 생성
-> 진짜 빈을 요청하는 위임 로직 (객체 조회를 필요 시점까지 지연 처리가 핵심)
-> 실제 싱글톤 객체와 다르게 동작하기 때문에 주의해서 사용!

Test Tip!!

  • Assertions 객체는 상단에 선언하고 사용하는게 좋다.
Thank You for Visiting My Blog, I hope you have an amazing day 😆
© 2023 Ian, Powered By Gatsby.