Spring 5부터 제공되는 논블로킹 HTTP 클라이언트로 Mono, Flux등을 사용해 비동기 데이터 스트림을 처리합니다.기본적으로 Spring WebFlux에 가장 잘 맞습니다. 물론 Blocking처리도 가능합니다.
OpenFeign
OpenFeign은Spring Cloud에서 제공하는 HTTP 클라이언트로 마이크로서비스 간의 REST API 통신을 간편하게 만들어주는 라이브러리입니다. (MSA에 적절한 시스템)
@FeignClient(name = "payment-service") // Eureka에 등록된 서비스 이름
public interface PaymentClient {
@PostMapping("/payments")
PaymentResponse makePayment(@RequestBody PaymentRequest request);
@GetMapping("/payments/{id}")
PaymentResponse getPayment(@PathVariable("id") Long id);
}
Spring Cloud의 모듈 Eureka를 쓰는 경우 name으로 서비스 와 매핑을 시킬 수 있습니다.
스코프란 스프링 컨테이너에서 빈을 관리하는 영역을 이야기한다. 기본적으로 스프링은 싱글톤 스코프를 가지고 이걸 가장 많이 사용한다. (이번 챕터는 그렇게 핵심은 아니지만 알아둬서 나쁠 거 없는 내용이고 언젠가는 쓰이는 내용이긴 하다) 참고로 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다
📝프로토타입
싱글톤이 아니라 빈의 생성하고 의존관계 주입까지만 관여하고 더는 관리하지 않는 범위이다. 그렇기 때문에 직접 종료를 해줘야한다. 그렇지 않으면 엄청나게 메모리에 쌓일 것이다. 예를 들자면 다양한 클라이언트가 어떤 Bean을 요청했을 때 일반적인 싱글톤의 경우 해당 Bean을 공유해서 사용한다. (때문에 도중에 값을 수정하면 다른 사람에게도 영향이 감) 반면 프로토타입의 경우 요청마다 Bean을 새롭게 생성해서 독자적인 스코프 영역을 만든다.
// prototype 사용예제1
@Scope("prototype")
@Component
public class HelloBean {}
// prototype 사용예제2
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
📝싱글톤에서 프로토타입 빈을 사용 주의점
싱글톤으로 생성된 Client Bean이라는 객체가 있고 의존관계 주입된 Bean이 프로토타입일 경우 ClientBean을 이용해 의존관계 주입된 Prototype으로 만들어진 Bean의 필드값을 바꾸는 경우를 생각해보자
일반적으로 Prototype이라서 서로 다른 클라이언트가 요청해도 필드값은 +1해도 각각 +1씩 가져야하지만 Client Bean이 넓은 범위 싱글톤이기 때문에 Prototype도 공유하게 된다.
📝 Prototype 유지하기 (Provider)
public class PrototypeProviderTest {
@Test
void providerTest() {
AnnotationConfigApplicationContext ac = new
AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
assertThat(count2).isEqualTo(1);
}
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@Scope("prototype")
static class PrototypeBean {
private int count = 0;
public void addCount() {
count++;
}
public int getCount() {
return count;
}
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init " + this);
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
위와 같은 문제를 Provider라는 걸 이용해 해결이 가능하다. 일반적인 방법으로는 ClientBean에 있는 logic을 실행시킬때마다 Prototype이 자동으로 의존관계를 주입하게 하는게 아니라 내가 직접 스프링 컨테이너에서 찾아서 넣어주게끔 한다 ac.getBean(PrototypeBean.class); 이러한 과정을 DL(Dependency Lookup)이라고 한다. 위 코드는 DL을 내가 직접해주는게 스프링에서 제공해주는 게 있다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
스프링에서는 ObjectProvider는 빈을 컨테이너에서 대신 찾아주는 DL 서비스 제공해준다.
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
또 다른 방법으로는 Java표준인 jakarta.inject.Provider을 사용하는 것이다. 얘는 따로 라이브러리 추가를 해주긴 해야한다.
📝 웹스코프
웹스코프란 웹에서 요청이 들어왔을 떄 언제까지 유지할지에 대한 범위이다.
request
HTTP Request와 동일한 생명주기에 요청마다 별도 빈 인스턴스가 생성되고 관리된다.
session
HTTP Session과 동일한 생명주기
application
서블릿 컨텍스트와 동일한 생명주기
websocket
웹 소켓과 동일한 생명주기
// Controller
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
// Logging Common
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + message);
}
@PostConstruct
public void init() {
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean create:" + this);
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
// Service Area
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
위 예제는 여러사용자가 많이 방문하면 로깅이 많이 찍힐텐데 동시다발적인 경우 어떤 곳에서 에러가 발생했을 경우 어떤 흐름에서 끊겼는지 구분해줄 수 있다. (HTTP Request Bean을 독자적 관리하기 때문에 구분 가능) 위 코드를 사용하면 MyLogger를 의존관계 주입을 하려고했지만 Request Scope라서 Request요청이 들어와야 생성된다. 이럴 경우 MyLogger가 필요한 곳에서 넣지못해서 에러가 나게 된다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
...
이러한 에러는 ObjectProvider로 해결이 가능하다 ObjectProvider는 스프링이 띄워질 때가 아니라 뒤늦게 요청이 들어와야 동적으로 만들어지거나 뒤늦게 조회가 필요한 경우 지연 조회 처리가 가능하다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
더 간단한 방법이 있는데 Proxy를 이용해 가짜 빈을 넣어줘서 있는 것처럼 동작하게끔한다. 그래서 에러가 나지 않는다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired // 생성자 1개일 때 생략 가능
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
생성자를 통해 의존관계를 주입 받습니다. 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입이 됩니다. 여기에선 빈으로 생성된 MemberRepository, DiscountPolicy가 의존관계 주입되어 들어가게 됩니다.
수정자 주입
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
setter를 통해 의존관계를 주입 받습니다. 여기에선 빈으로 생성된 MemberRepository, DiscountPolicy가 의존관계 주입되어 들어가게 됩니다.
필드 주입
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
필드에 바로 주입하는 방법입니다. 코드가 간별하지만 외부에서 변경이 불가능해서 테스트 하기 어렵다는 단점이 있다. 예를 들면 setDiscountPolicy, setMemberRepository같은 Setter가 존재하면 DI 컨테이너(스프링)를 따로 안 띄워도 직접 넣어서 테스트할 때 사용이 가능한데 필드만 있는 경우는 스프링을 다 띄우지 않는 이상 테스트하기 어렵다.
📝어떤 걸 사용해야할까?
생성자 주입을 사용해야합니다. 필드 주입의 경우 테스트가 어려우며 Setter의 경우 변경가능성이 있기 때문에 객체 생성시 1번만 호출되게 설계할 수 있습니다.(불변) 또한 순수 자바코드로 단위테스트도 가능합니다.
final 키워드 사용시 생성자에 값이 없으면 컴파일 오류를 발생시켜 장애를 방지할 수 있습니다.
DiscountPolicy를 상속받은 RateDiscountPolicy, FixDiscountPolicy가 있으면 위 코드와 같이 했을 때 필드명을 보고 주입하게 된다. 즉, RateDiscountPolicy를 주입하게 된다.
@Qualifier
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
Qualifier의 경우 @Qualifier에 등록한 Bean이름을 찾아서 주입합니다.
// 어노테이션 생성
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}
위 같이 @Qualifier지정할 때 따로 어노테이션을 따로 만들어서 사용할 수 있는데 이렇게 사용하면 컴파일시기에 에러를 잡아낼 수 있는 장점이 있다.
@Primary
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
Primary의 경우 여러개 빈이 있어도 Primary 등록한 최상위 것이 먼저 등록됩니다. Qualifier의 경우 코드가 지저분해지기 때문에 @Primray를 더 잘 사용합니다.
📝등록된 Bean 다 가져오기
public class AllBeanTest {
@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;
private final List<DiscountPolicy> policies;
public DiscountService(Map<String, DiscountPolicy> policyMap,
List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
System.out.println("discountCode = " + discountCode);
System.out.println("discountPolicy = " + discountPolicy);
return discountPolicy.discount(member, price);
}
}
}
DiscountPolicy 인터페이스를 상속받은 고정할인(fixDiscountPolicy)하고 퍼센티지할인(rateDiscountPolicy)이 Bean으로 만들어져있습니다. 여기에 사용자가 넘긴 정보에 따라 고정 또는 퍼센티지를 결정해야할 경우 DiscounyPolicy구현체인 Bean으로 만들어진 걸 가져와서 넘긴 파라미터에 따라 다르게 처리할 수 있습니다.
Map의 경우 키에 Bean의 이름이 들어가고 값에는 구현체가 들어가게 됩니다. List의 경우는 키값이 없기 때문에 구현체만 들어가게 됩니다.
📝 수동 등록 vs 자동 등록
수동등록
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
Configuration을 쓰고 Bean을 그 안에서 직접 생성해서 관리합니다. 한 곳에 모여있기 때문에 보기가 편합니다.
자동등록
@Component
public class DiscountPolicy {
}
Component어노테이션 기반으로 만들어진 어노테이션을 붙이면 Component Scan범위에 있으면 찾아서 Bean 등록해줍니다
요즘은 자동등록을 많이 쓰지만 수동등록의 경우도 필요할 때 써야한다. 보통 업무로직(Controller, Service, Repository)의 경우 자동 등록을 사용하고 기술지원(로깅, AOP 등)의 경우 Configuration으로 수동관리하는 편이다.
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class MyClass {
private final String name; // 생성자 매개변수로 포함됨
}
// 생성된 생성자:
// public MyClass(String name) {
// this.name = name;
// }
final로 된 필드기반으로 생성자를 생성합니다.
@Getter
import lombok.Getter;
@Getter
public class MyClass {
private String name;
private int age;
}
// 생성된 메서드:
// public String getName() { return name; }
// public int getAge() { return age; }
필드기반으로 Getter를 생성합니다.
@Setter
import lombok.Setter;
@Setter
public class MyClass {
private String name;
private int age;
}
// 생성된 메서드:
// public void setName(String name) { this.name = name; }
// public void setAge(int age) { this.age = age; }
필드기반으로 Setter를 생성합니다.
@ToString
import lombok.ToString;
@ToString
public class MyClass {
private String name;
private int age;
}
// 생성된 메서드:
// public String toString() { return "MyClass(name=" + this.name + ", age=" + this.age + ")"; }
필드기반으로 toString()메서드를 자동으로 생성합니다. 로깅시 객체안에 있는 값을 편하게 볼 수 있습니다.
@Data
import lombok.Data;
@Data
public class MyClass {
private String name;
private int age;
}
// 생성되는 메서드:
// - Getter/Setter for name and age
// - toString(), equals(), hashCode()
@Configuration과 @Bean을 이용해도 되지만 점점 많아지게되면 힘들고 파일 하나에 엄청난 @Bean이 생겨날 수 있다.
이걸 깔끔하게 만드는 법이 컴포넌트 스캔이다. 컴포넌트 스캔은 @Component가 붙은 정보를 Bean으로 등록하게 된다.
컴포넌트 스캔
@Configuration
@ComponentScan(
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
}
컴포넌트
@Component
public class MemoryMemberRepository implements MemberRepository {}
이렇게 되면 @Component가 붙은 MemoryMemberRepository를 Bean으로 등록할 수 있다.
📝@Bean vs @Component
@Bean은 이제 더이상 쓸모 없는 행동일까? 어떤 상황에 @Bean을 쓰고 어떤 상황에 @Component를 쓰는지 알아보자
Bean 등록 방식 (@Bean, @Component)
// 이전 방식 사용
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyService(); // MyService를 빈으로 등록
}
@Bean
public MyComponent myComponent() {
// MyComponent가 MyService를 필요로 한다고 명시적으로 주입
return new MyComponent(myService());
}
}
//이후 방식 사용
@Component
public class MyService {
// 빈으로 등록될 클래스
}
@Component
public class MyComponent {
private final MyService myService;
// @Autowired를 통해 Spring이 자동으로 MyService 빈을 주입함
@Autowired
public MyComponent(MyService myService) {
this.myService = myService;
}
}
비교하기 위해 동일한 빈 등록하는 방식을 Bean과 Component로 보여줬다.
@Bean
@Configuration이 필요하다
메소드 단위에 붙이게 된다. 해당 메소드는 클래스를 반환해야한다 (Component에서 해당 클래스가 @Component가 된다)
의존관계 주입을 직접 Inject을 하기 때문에 Autowired를 통해 Injection할 필요가 없다
MyComponent에 myService()를 매개변수로 직접 넣어주고 있다.
@Component
클래스 단위에 붙이게 된다. 해당 클래스는 빈으로 등록되게 된다.
의존관계 주입시 @Autowired를 사용한다
일반적으로 @Component를 많이 사용한다. @Bean 방식하고 비교하면 코드수도 확연히 줄어들게 된다. 하지만 @Bean도 필요할 때가 있다.
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource(); // 외부 라이브러리 클래스 인스턴스 생성
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
dataSource.setUsername("username");
dataSource.setPassword("password");
return dataSource;
}
}
HikariDataSource의 경우 외부 라이브러리이기 때문에 내가 직접 @Component를 붙일 수가 없다. 스프링에서 관리가 필요한데 이럴 경우 위 코드와 같이 직접 HikariDataSource의 객체를 만들고 return시킨 이후에 해당 것을 빈객체로 등록하게 되면 된다. 이럴 경우 HikariDataSource를 new해서 사용하면 안 되고 내가 Bean으로 등록한 DataSourceConfig의 dataSource()를 사용해야한다.