BackEnd/spring

[인프런] 스프링 핵심원리 기본편(김영한) 정리

하용권 2023. 7. 28. 17:27

이번에 코로나에 걸리면서 일주일 동안 휴가를 가지게 되었습니다.

 

이 기간 동안 인프런의 김영한님 강의를 전부(기본, 고급)을 볼 계획입니다.

 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

 

이는 기본편을 듣고 제가 보기 편하게 정리한 내용입니다.

 

기초(김영한)

스프링 부트 장점

애플리케이션 쉽게 생성가능. tomcat이 내장임

외부 라이브러리(3rd path) 자동 구성

 

SOLID

OCP를 지키려면 객체를 생성하거나 그러한 것들을 해주는 무언가가 있어야 함.(스프링 DI도 가능)

DIP 추상화에 의존. 구체화에 의존 x. 역할만 알면 되고 구체적인 객체까진 몰라도 됨.

너무 기초.

 

추상화를 위해 인터페이스를 만들면 비용 발생.

→ 확장할 기능 없다면 구체 클래스 직접 사용. 추후 리팩터링 하도록.

추상적인 것과 구체적인 객체 둘 다 의존하는 것은 좋지 않음(DIP 위반)

→ 이렇게 되면, 다른 객체로 교체하려면 수정을 해야 하는 일이 생길 수가 있음(OCP 위반)

 

관심사의 분리

필드에서 직접 new를 통해서 객체를 주지 않도록.

그래서 AppConfig 같은 클래스를 만들어서 이를 주입하도록 수정.

그리고 생성자를 통해서 주입함.

 

구체적인 어떤 클래스가 주입될 지는 외부에서 결정이 됨.(AppConfig)

→ dependency injection

 

만약 할인 전략이 바뀐다면, 이에 해당하는 객체를 만들어주고 AppConfig 부분만 수정해주면 됨. 매우 간단함.

OCP도 만족이 됨. 이를 사용하는 클라이언트 코드는 수정이 필요가 없음.

IoC, DI, 컨테이너

IoC는 프레임워크 같은 곳에서 뭔가를 해주는 것(제어권 역전)

프레임워크는 제어를 알아서 해줌(junit 도)

 

라이브러리는 개발자가 직접 제어.(main 등)

 

정적인 의존 관계는 어플리케이션을 실행 안해도 알 수 있음(import)

동적인 의존 관계는 실행 시점에 결정됨.

 

DI는 런타임(실행 시점)에 객체를 만들고 주입하게 됨.

장점은 앞에서 본 것 처럼 클라이언트 코드 변경이 없음. 그리고 동적인 객체 관계를 쉽게 바꿀 수 있음.(어플리케이션 코드를 손을 대지 않는다는 뜻)

객체 생성하고 DI를 해주는 것을 IoC 컨테이너 혹은 DI 컨테이너라고 함.

(최근에는 DI컨테이너라고 한다고 함)

 

ApplicationContext(스프링 컨테이너임)에서 bean 정보를 가져올 수 있음.

bean이름은 method이름으로 되어 있음.

(물론 Bean(name = “…”) 으로 이름 바꿀 수 있음)

 

주의할 점은 bean 이름 중복되지 않도록.

applicationContext는 beanFactory를 상속받음.

 

싱글톤의 주의점은?

여러 클라이언트가 하나의 객체를 공유하게 됨.

→ stateless 로 설계해야함. 필드 대신 지역변수가 thread local 같은 것을 이용해야 함.

(race condition 피하려고)

 

bean annotation이 있다면,

→ 이미 존재할 경우 기존 객체 반환

→ 없다면 기존 로직 작동.

그래서 싱글톤으로 작동하게 됨.

 

Configuration 어노테이션이 없다면, 싱글톤이 아니게 됨. 호출될 때 마다 새로운 객체 만들어버림.

작동은 함.

 

컴포넌트 스캔은 @Component가 붙은 클래스를 스캔해서 자동으로 빈 등록

(Configuration도 포함. Component가 이미 붙어있기 때문)

→ 자바에서는 annotation을 상속 받는다는 개념이 없음. 이는 스프링의 기능.

 

basePacakages로 검색 시작할 패키지 지정가능 → 이는 속도 향상(원래는 모든 자바 코드 뒤져봄)

디폴트는 ComponentScan이 붙은 설정 정보 클래스의 패키지.

→ 그래서 최상단 패키지에 AppConfig 같은 설정 정보를 넣고, ComponentScan을 붙임.

 

Configuration을 쓰지 않고, 직접 class에 Component를 붙임으로써 bean으로 등록이 가능함.

→ 그렇다면 필드에 bean을 주입하기 위해서는 어떻게?

→@Autowired를 쓰면됨.(근데 없이 그냥 생성자만 이용해도 되지 않나? → 뒤쪽에서 설명해주신다고 함.)

생성자가 한 개라면, 자동으로 주입됨.

 

@Component의 이름은 디폴트로 클래스명의 맨 앞글자는 소문자로 변경됨.

(물론 직접 정의 가능함)

 

빈 이름은 무조건 이름 다르게.

 

Component vs Configuration에서 bean으로 등록하는 경우

수동 등록 빈이 우선임.(후자) 오버라이딩 해버림. 최근 스프링 부트에서는 이를 에러 발생하게 함.

여담) 개발자는 애매하게 하지 말고 명확하게 해야함.


의존성 주입(DI)

 

생성자

호출 시점에 1번 실행됨. → 바뀌지 않도록 할 수 있음. (불변, 필수 일 경우 사용)

생성자가 1개라면, autowired 필요없음.

처음에 생성자로 주입이 되고, 그 뒤에 setter(수정자)로 다시 한번 주입이 됨.

(싱글톤이기 때문에 물론 같은 객체가 들어옴)

 

setter는 autowired에서 required false로 하여, 선택적으로 빈을 사용할 수 있게 할 수 있음.

필드에 autowired를 선언하여 넣을 수 있음.

→ 이는 추천하지 않음.

→외부에서 변경이 불가능하여 테스트하기 힘듦.(mock을 사용 못하나 봄?)

→ 그리고 스프링 컨테이너 없이 테스트할 시에, 아무런 값이 들어가지 않음.

→ 테스트 코드를 짤 때는 사용하는 편임 → 테스트할 때만 이용하니까 상관이 없음.

 

autowired 붙여서 일반 메서드에서도 주입가능함.

하지만 거의 사용 안함.

autowired required false를 하면 호출자체가 안됨.

null을 넣고 싶으면 Nullable을 하면, null이 들어감.

Optional<T>를 하면, Optional.empty() 가 됨.

특정 필드에만 nullable, optional해도 됨.

 

이 중에서, 생성자 주입을 선택해야 하는 이유는?

  1. 불변.

실행 되고는 바뀌지 않음.

테스트할 때에는 생성자를 통해서 다른 객체를 넣을 수 있어서 단위테스트에도 좋음.

생성자를 하면 코드가 많아짐.

롬복을 이용하면 됨 ㅎㅎ..(RequiredArgsConstructor)

(근데 세터나 게터는 주의가 필요하지 않나.)


조회할 빈이 2개 이상이라면?

에러 발생함. 그렇다고 하위 타입으로 직접 지정하면 DIP가 깨짐.

→ 파라미터로 들어오는 이름을 구체 클래스 이름으로 바꾸면 됨.(e.g discountPolicy → rateDiscountPolicy) 필드 이름을 바꿔도 되긴 함. 이 부분은 별로 추천하지 않음.(DIP를 결국엔 해치는 느낌)

 

@Qualifier로 bean에 추가 구분자 부여 가능.

빈에다가도 해주고, 파라미터/필드에도 하면 됨.

만약 해당하는 Qualifier 빈을 찾음.

@Primary로 우선순위 지정 가능

둘 중 우선순위는 qualifier가 높음.

Qualifier는 문자열이라서 컴파일 타임에는 체크가 안됨.

→ 이를 보완하기 위해 어노테이션 이용.

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("hiBean")
public @interface CustomAnnotation {
}

하면 됨. 그리고 이 어노테이션을 이용하면 됨.

(스프링은 이러한 어노테이션 상속이 가능함. 순수 자바 기능 아님!)


모든 빈 가져오기

그냥 list로 선언하고 생성자 파라미터로 list를 넣어주면 됨.

Map으로 해도 됨. 이 때 key는 String이고 빈 이름임.

 

만약 특정 클래스를 빼고 싶다면?

이는 올마른 디자인이 아니라고 생각함.(개인적인 생각) 그래서 이러한 기능보다는 클래스 구조를 바꿔야할 것 같음.

 

자동 빈(@Component)으로 해도 OCP, DIP는 지켜짐.

그러면, 수동빈(@Configuration, @Bean) 등록은 언제?

→ 업무 로직 빈(repository, service, controller 등)에서는 자동 빈이 좋음. 빈들이 너무 많음.

→ 기술 지원 빈(db 연결, AOP, 공통 로그처리) 에서는 수동 빈. 이는 수가 적고 전체적인 어플리케이션에 광범위 영향을 미침.(security 설정 느낌인가)

 

비즈니스 로직에서 다형성을 이용할 때는 수동 빈이 좋을 수 있음.

여러 빈 가져올 경우.

→ List<CustomClass> 할 경우, CustomClass를 하나하나 전부 찾아봐야 함.

별도의 Config 클래스를 만들어서 처리하면, 어떠한 빈들이 등록되어 있는지 한눈에 확인할 수 있음.

 


스프링에서 의존관계 주입이 되면, 콜백 메서드를 통해서 초기화 시점을 알려줄 수 있음.

컨테이너 종료 전에도 콜백을 통해 알려줄 수 있음.

 

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

 

객체 생성과 초기화는 별도로 두는 것이 좋음.

생성자는 필수 정보를 받아와서 메모리를 할당하고 객체를 만드는 역할.

초기화는 이런 정보를 활용해서 무거운 작업(db 연결 등) 수행

db연결 같은 무거운 작업은 생성자보다는 별도의 메소드에서 하는 것이 유지보수하기 더 좋다고 함(왜? 이 부분은 잘 모르겠음)

 

InitalizingBean(초기화 콜백)에 있음. 이를 implements 하면 됨(afterPropertiesSet 오버라이딩).

DisposableBean(소멸 콜백). 이를 implements하면 됨.(destroy 오버라이딩)

단점은 외부 라이브러리에 적용하기 힘듦. 그리고 코드레벨에서 직접 수정해서 좀 그럼. 그리고 스프링에 의존적임.

지금은 이러한 방법 사용 거의 안함.

 

@Bean(initMethod = “메소드 이름”, destroyMethod = “메소드 이름”)

으로 가능함.

장점은 빈에 의존하지 않음. 코드를 고칠 수 없는 외부라이브러리에도 적용 가능.

디폴트 값은 (inferred) 임.

외부 라이브러리 대부분 close, shutdown을 이용함. 이러한 이름이 있다면 자동으로 호출해줌.

 

메소드에 @PostContruct, @PreDestroy 이용하면 됨 그냥. 이 방법을 추천한다고 함. 스프링에 종속적인 기능이 아님.(패키지 보면 좀 다름)

단점은 외부 라이브러리에 사용 못함. 코드를 수정해야 하기 때문에.


빈 스코프

빈은 기본적으로 싱글톤으로 만들어짐. 컨테이너의 시작부터 종료까지 살아있음.

프로토타입은 빈 만들어주고 의존관계까지만 주입해주고 컨테이너가 더 이상 관리 안함.

 

웹 관련도 있음

request : 요청 들어오고 나갈 때까지 유지.

session : 세션 동안.

application : 서블릿 컨텍스트(?)와 같은 범위

 

싱글톤은 빈을 요청하면 이미 만들어둔 객체를 반환함.

 

프로토타입은 요청하면 새로운 객체를 만들고 이를 줌. → 컨테니너가 관리를 안하니까 이미 존재하는 객체를 줄 수가 없음. → 초기화까지만 처리하기 때문에 PreDestroy가 발생하지 않음. 있어도 호출을 안함. 종료는 해당 빈을 이용하는 객체(클라이언트)가 함.

 

싱글톤 빈에서 프로토타입 빈을 사용하게 되면?

싱글톤 빈은 생성 시점에 주입을 받음. 그래서 생성 시점에만 프로토타입 빈을 새로 만들게 됨.

사용할 때는 이미 만들어진 프로토타입 빈을 사용하게 됨. (물론 요청하면 새로 만들겠지만.)

 

사용할 때마다 새로운 빈을 주입받고 싶다면?

쉬운 방법은 사용할 때마다 컨테이너(ApplicationContext 같은)에 프로토타입 빈을 요청하면 됨.

→ 하지만 이 방법은 지저분함. 컨테이너에 종속적임.

→ 프로토타입 빈을 컨테이너에서 찾아주는 역할이 필요함(DL, dependency lookup, 의존관계 조회)

 

Provider를 이용하면 됨.

ObjectProvider<프로토타입 빈 클래스>ObjectFactory<프로토타입 빈 클래스> 이용하면 됨.

필드에 추가하고, 의존성 주입 받으면 됨. 그리고 실제 로직에서 Ojbect~~.getObject() 하면 빈 가져옴. → 스프링 컨테이너를 통해 빈을 찾음 → 프로토타입 빈이기 때문에 새로 만들어서 가져오게 됨. → 단위테스트나 mock이용하기 편해짐.

factory에 편의 기능 더한 것이 provider. stream 처리 등 추가됨. 둘 다 스프링에 의존적.

 

스프링에 의존하지 않는 새로운 기술인 javax.inject.Provider 등장함.(자바 표준)

.get()호출하면 컨테이너에서 찾아서 빈 가져옴.(프로토타입이라 새로 만들게 됨).

별도의 라이브러리가 필요함.

 

 

프로토타입 빈은 언제 사용?

사실 거의 사용 안함.

순환참조 같은 걸 방지할 수 있고 lazy하게 가져오기 가능.(코드에 주석으로 직접 설명이 되어 있음)

 

웹 빈

스코프가 request 같은 빈을 의존 관계 주입해야 할 경우.

만약 Controller나 Service 같은 bean에서 생성자를 통해 해당 request 빈을 주입하려고 하면?

→ 에러 발생. → 사용자가 요청을 해야만, 빈이 생성되기 때문에 컨테이너에서 해당 빈을 찾지 못해서 에러가 발생함.

이는 앞처럼 Provider로 해결가능

(프로토타입은 에러는 발생 안했음. 싱글톤 빈에서 주입할 때, 컨테이너에서 새로운 빈 만들어서 주입하기 때문에. 반면 request는 사용자 요청이 와야지만 만들어져서 이러한 에러가 발생함)

 

Provider가 getObject()를 할 때 bean이 만들어짐(lazy)

그리고 같은 요청이라면, Service나 Controller 모두 같은 객체 이용함.

 

이러한 빈 사용 이유는?

컨트롤러처럼 웹에 의존적인 부분은 이러한 빈을 사용함으로써 service같은 다른 계층으로 웹 정보가 넘어가지 않게 됨. 즉 유지보수하기 쉬워짐.

 

Provicer 대신에 @Scope(proxyMode = ScopeProxyMode.TARGET_CLASS 또는 INTERFACE) 이용하면 됨. (프로토타입도 가능한가?)

이는 진짜가 아닌 가짜를 주입해줌. 실제 객체를 이용할 경우 진짜 빈을 찾아서 작동하게 됨(JPA의 lazy 느낌) 스프링의 CGLIB라는 바이트 코드를 조작하는 라이브러리를 이용하여 이를 가짜를 만듦.

프록시 객체는 싱글톤임.

→ 하지만 내부적으로는 빈이 전부 다르게 생성이 됨. 실제 객체는 범위를 따름. 싱글톤이 아님.주의해야 함.

웹 스코프가 아니여도 프록시 객체 사용가능 하다고 함.

→ 이는 클라이언트 코드를 고치지 않음 → 유지보수하기 매우 좋아짐. → 다형성과 DI 컨테이너가 가진 장점임.

 

 

느낀 점

객체지향을 잘 알아야지만 스프링의 본래 기능을 사용할 수 있을 것 같음.

깔끔한 코드나 느슨한 결합을 하기 위해 좋은 디자인이 필요.

 

강의하시는 분이 정말 대단한 것 같음. 스프링을 왜 사용하고 이에 대해 설명해주는 것이 좋았음. 특히 스코프에서 특별한 스코프를 가진 빈을 사용하는 이유도 설명해주고, 기존 코드를 구현하고 문제점을 보완하기 위해 스프링을 이용하는 것이 인상깊었음.

반응형