Spring Cloud Config Server

JAVA/Spring|2019. 6. 25. 19:00

요즘 Spring Boot로 배포함에 따라 jar 하나만 배포하는 경우가 많습니다.

때문에 Config 파일은 어떻게 배포해야할지 고려해야할 부분들이 생겼고

 

간단하면서 단순한 두가지 방법이 있습니다.

 

1. Jar에 설정 파일을 포함한다.

   jar내에 config 파일을 포함시켜 배포할 수 있지만 설정을 변경을 해야하는 경우 재배포 하거나

   jar내에 config 파일을 수정할수도 있습니다.

   하지만 간단히 설정만 바꾸고자 하는데 너무 과분하다고 생각이 듭니다.

 

2. 외부에 config 파일을 두고 boot를 띄울때 커맨드를 주어 config를 잡는다.

   java -jar example.jar --spring.config.location:....

  설정파일이 외부에 있기 때문에 설정하기에는 편합니다.

  하지만 배포해야할 서버, 설정을 변경해야 할 서버가 많아진다면 

  노가다이기도 하고, 변경하다가 놓치거나 잘못 변경하는 실수가 발생할 수 있습니다.

  물론 변경 내역 관리가 안되는건 덤입니다.

 

위에 문제점들을 개선할수 있는 방법으로 Spring Cloud Config Server를 활용할수 있습니다.

 

'Spring Cloud Config Server' 란

 

각 서버가 Config 파일을 직접 관리하거나 가지고 있는것이 아니라 'Spring Cloud Config Server'를 통해 설정을 가져오며 'Spring Cloud Config Server'는 설정 파일을 git이나 파일 스토리지에 관리하여 Client 요청에 따라 config 내용을 전달해줍니다.

 

'Spring Cloud Config Server'를 활용하게 되면 App(그림에서는 Microservice) 배포시에

각종 설정들(DB나 기타 설정들)은 고려하지 않아도 되며 'Spring Cloud Config Server' 과 profile을 통해 각 설정들을 얻어올수 있습니다.

 

아직  코드를 깊게 보지 않았으나

조금 사용 해봤을때 일단 설정은 굉장히 간단하며

 

추후 깊게 볼 필요(기본적으로 지원하는 git이나 file이 아니라 커스텀 작업을 해야한다면)가 생기면 추가로 살펴보려고 합니다.

잠깐 살펴보니 JDBC Backend랑 있네요

https://cloud.spring.io/spring-cloud-config/2.1.x/single/spring-cloud-config.html#_jdbc_backend

 

 

참고로 'Spring Cloud Config Server' 를 WebFlux로 띄워볼까 하여 시도해봤지만

@EnableWebFlux 옵션을 추가하면 기동시 에러가 발생합니다.

에러와 Spring starter에서 'Spring Cloud Config Server'만 선택시 tomcat이 함께 포함되는것을 봐서는 내부에 @EnableWebMvc가 포함되어있는것으로 예상됩니다.

 

 

설정

'Spring Cloud Config Server'를 사용하기 위해서는

 

1. @EnableConfigServer 설정

2. git url 및 접속정보 설정(private일 경우 userusername, password 설정 필요)

 

3. git에 설정파일 push(설정파일은 프로젝트명-profile.yml)  , yml아니라 properties도 가능

4. url로 테스트(url:port/프로젝트명/profile)

 

refresh 주기라던가 즉시 refresh 등 실제 사용에 필요한 부분들은 

차차 필요시 알아볼 예정입니다.

 

'Spring Cloud Config Client' 는 건너뛸수 없으니 조만간 블로그로 정리 예정입니다.

댓글()

[SpringBoot] Embedded Tomcat, NioEndpoint

JAVA/Spring|2019. 6. 11. 19:22

SpringBoot Async 처리를 다시 학습 중에

어떻게 Embedded Tomcat에서는 요청을 처리하고 있는지 파악하기 위해 조금 코드를 살펴봤다.

 

 

일단 크게 별다른 설정을 하지 않고 디버그 모드로 붙었을 때 아래와 같이 스레드가 떠있다.

대략 오늘 살펴보려는 스레드는 4가지 정도다

 

  • http-nio-8080-Acceptor-0
    • Http Port accept 하는 스레드(ServerSocket.accept())
    • 관련 클래스는 org.apache.tomcat.util.net.Acceptor
    • accept 되면 getpoller() 후 채널을 regist 한다
      • getpoller는 poller가 여러 개의 경우 rr로 동작한다.
  • http-nio-8080-ClientPoller-0~1
    • Accept 된 Socket을 Selector에 OP_READ를 등록하고 Selector를 epoll 한다.
    • 관련 클래스는 org.apache.tomcat.util.net.NioEndpoint$Poller
    • 요청이 들어오면(read가 불리면), ThreadPoolExecutor.execute를 부르게 되고, 결국에 TaskQueue에 offer()를 부르게 된다.
    • poller가 하나가 아니라서 어떤 기준으로 동작 하나 살펴보니 아래와 같이 값이 설정되어있으며
      • polle는 많아도 2개
      • 기본 동작은 selector는 select()로 blocking이 아닌 select(1000), 1초마다 깨어 난다.

  • http-nio-8080-exec-1~10
    • 실제 ServletThread
    • TaskQueue(BlockingQueue)를 take 하며 대기, poller 혹은 다른 곳에서 TaskQueue에 이벤트를 넣어주면 동작
    • 선후 처리 및 Controller 코드가 보통 이 스레드를 통해 불리게 되며 Async 처리 이후 다시 돌아와(받은 스레드가 대기하여 다시 돌아온다는 이야기는 아니고, ServletThread->Async->ServletThread) 처리
    •  

  • MvcAsync
    • Controller에서 Async 처리(Callable이라던지 기타 등등..) 이후 응답을 직접 하려나? 해서 살펴보니
    • 마지막에 다시 TaskQueue에 offer를 하고 있었다.

 

 

전체 처리를 대략적으로 그리면 아래와 같이 될 것 같다.

 

 

댓글()

[Spring][Thymeleaf] if else그리고 조건문

JAVA/Spring|2019. 5. 22. 19:30

Spring Boot에서 View를 주로 Thymeleaf를 사용하고 있다.

 

생각 안나는 것들은 필요할때마다 검색해서 사용하고 있기 때문에

 

정리해본다.

 

예제는 dutch 값이 모델에 있을경우 dutch.name을 확인 후 selected 세팅을 하도록

 

단순 if

<option  th:if="${dutch!=null}" 
th:each="coffee : ${T(com.meteor.coffee.CoffeeEnum).values()}" 
th:value="${coffee.name()}" th:text="#{${coffee.name()}+'.name'}"
th:selected="${coffee.name() == dutch.coffeeKind.name()}">
</option>

 

 

if-else

<option  th:if="${dutch!=null}" 
th:each="coffee : ${T(com.meteor.coffee.CoffeeEnum).values()}" 
th:value="${coffee.name()}" th:text="#{${coffee.name()}+'.name'}"
th:selected="${coffee.name() == dutch.coffeeKind.name()}">
</option>

<option  th:unless="${dutch!=null}" 
th:each="coffee : ${T(com.meteor.coffee.CoffeeEnum).values()}" 
th:value="${coffee.name()}" th:text="#{${coffee.name()}+'.name'}">
</option>

 

여기서 기억에 남는건 unless에 들어가는 조건이 위의 if 조건에 들어간것과 같아야 한다는것

 

 

추가적으로, 변수 내에서도 분기가 가능하다

<form method="post"> 에서 method를 분기 처리한다면

 

<form method="${dutch==null ? 'post':'put'}"> 처럼 표현이 가능하다.

물론 put은 

<input type="hidden" name="_method" value="put">

으로 표현되겠지만..

 

 

댓글()

@ModelAttribute, Controller 메소드에 붙였을때

JAVA/Spring|2019. 5. 20. 18:41

@ModelAttribute, Controller 메소드에 붙였을때 요청이 들어오면 들어온 요청 Model에 메소드 리턴값이

들어오는것을 확인할 수 있다.

 

Object dutchMap = model.asMap().get("dutchMap");

@ModelAttribute("dutchMap")
	public Map<String,DutchDao> getDutchMap(){
		Map<String,DutchDao> map = new HashMap<>();
		map.put("k11", new DutchDao());
		map.put("k12", new DutchDao());
		map.put("k13", new DutchDao());
		return map;
	}

그런데 하나 궁금점

 

위에 getDutchMap()의 결과인 Map<String,DutchDao>값을

Controller 생성 초기에 bean으로 등록해서 가지고(캐싱하고) 있을까?

 

아니면 매번 요청마다 getDutchMap()을 부르게 될까?

 

확인 방법은 간단하게 getDutchMap()을 break point를 찍고 확인해보니..

 

요청마다 getDutchMap() 를 부르고 있었다..;;;

 

실시간으로 매번 생성해서 model에 세팅하는 용도라면 적절할지 모르겠지만..

모든 요청마다 매번 메소드 불리기 때문에 주의해서 사용해야 겠다...

 

 

 

 

댓글()

[Spring] ApplicationEventPublisher, AbstractApplicationEventMulticaster

JAVA/Spring|2019. 4. 30. 19:00

지난번 글에서 Spring 에서 Event를 publish 할때

TaskExecutor가 설정되어있지 않은 경우 publish 하는 스레드에서 이벤트핸들러들을 부른다고 했다.

https://meteorkor.tistory.com/entry/ApplicationEventPublisher

 

Spring Event ApplicationEventPublisher

Spring에 Event 처리하는 부분이 있길래 의문점이 조금 생겨 코드를 살펴 봤다 물론 동작은 각 설정에 따라 변경될수 있겠지만 기본 설정(가능한 설정을 손 안댄 상태에서)에서 어떻게 동작하는가? "이벤트별 블럭..

meteorkor.tistory.com

TaskExecutor를 설정하면 당연히 설정된 TaskExecutor에 이벤트 수행을 위임하게 될것이다

그렇다면 TaskExecutor를 어디에 어떻게 설정 해야 할까?

 

일단 샘플로 해보려던건 이벤트를 publisher가 아니라 Event만 처리하는 단일 스레드를 만들어 처리하는 것!

 

역시나 코드를 까봐야 알수 있는데 

ApplicationEventPublisher 에 publishEvent(event)를 쭉 타고 가면

AbstractApplicationContext에 publishEvent(Object event, ResolvableType type) 쪽에 도달할 수 있다.

보면 결국 ApplicationEventMulticaster를 구해서 multicastEvent()를 부르는걸 볼수 있고

그안에서 결국 지난번 글에서 말한대로 Executor가 있으면 Executor를 통해 수행하고 없으면 invokeListener()를 통해 로컬 콜을 수행하는걸 볼수 있다.

 

위에 getTaskExecutor()를 보면  결국 ApplicationEventMulticaster가 들고 있는 TaskExecutor를 사용하는것을 확인할수 있었다

 

그럼 ApplicationEventMulticaster 는 어디서 TaskExecutor를 가져올까..?

빈으로 TaskExecutor를 등록하면 타입으로 가져와서 쓰려나..?

 

그런데 아무리 TaskExecutor 타입의 빈을 등록해도 ApplicationEventMulticaster 안에 TaskExecutor는 null이였다..

 

그럼 ApplicationEventMulticaster 타입의 빈을 등록해두면 ApplicationEventMulticaster을 등록한 빈으로 사용하려나? 했는데 마찬가지로 안됬다

 

결국 찾은 방법은

빈이름을 주고 등록하는것

"applicationEventMulticaster" 이름으로 고정되어있나보다..

 

 

그래서 위와같이 빈 이름을 주고 빈등록을 하면 위 구현체를 사용해서 Event를 Multicasting 하는것을 볼 수 있었다.

 

 

 

 

댓글()

[Spring] 인트로덕션(introduction), @DeclareParents

카테고리 없음|2019. 4. 29. 18:30

책을 보다 잘 모르던 기능이 보여서 정리 차원에서 글을 남긴다.

 

"인트로덕션"

어노테이션은 @DeclareParents 를 사용하며

다중 상속을 사용할 수  없는 상황에서 다중 상속처럼 여러 구현체를 사용할 수 있도록 해주는 기능

(사실 타겟 클래스에 설정된 인터페이스를 모두 implements 하게 하고 구현할 내용에 인터페이스 구현체들을 콜하는 방식으로 동적으로 구현해주는 기능, CGLib을 통해)

 

역시나 돌려보는게 이해하기 빠르다

 

예제 네이밍은 조금 이상하긴 하지만.. 설명을 붙이자면

Coffee 인터페이스와 Color 인터페이스가 존재하고

Coffee의 구현체인 Robusta와 Color의 구현체인 Black을 

인트로덕션을 사용해서 CoffeeColor 클래스가 Robusta와 Black을 상속 받은 것처럼 사용해보았다.

 

돌려보기 위해 준비할 클래스

 

0. config 클래스()

//@EnableAspectJAutoProxy 안해주면 에러난다..

package com.meteor.bbbb;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConfigTest {

}

1. introduction 될 클래스(TargetClass, ColorCoffee)

package com.meteor.bbbb;

import org.springframework.stereotype.Component;

@Component
public class ColorCoffee {

}

2. introduction 할 내용을 선언한 클래스(CoffeeIntroduction)

package com.meteor.bbbb;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class CoffeeIntroduction {

	@DeclareParents(value="com.meteor.bbbb.ColorCoffee", defaultImpl=Black.class)
	public Color col;

	@DeclareParents(value="com.meteor.bbbb.ColorCoffee", defaultImpl=Robusta.class)
	public Coffee coffee;
	
	
}

 

3. introduction에 사용되는 인터페이스(Coffee, Color)

package com.meteor.bbbb;

public interface Coffee {
	public void drink();
}

 

package com.meteor.bbbb;

public interface Color {
	public String getColor();

}

4. introduction에 사용되는 인터페이스 구현체 클래스(Robusta, Black)

 

package com.meteor.bbbb;

public class Robusta implements Coffee{
	@Override
	public void drink() {
		System.out.println("robusta drink");
	}

}
package com.meteor.bbbb;

public class Black implements Color{

	@Override
	public String getColor() {
		return "Black";
	}

}

 

마지막으로 돌려보는 테스트 코드

	@Test
	public void contextText2() {
		ApplicationContext context = new AnnotationConfigApplicationContext("com.meteor.bbbb");
		Coffee coffee  = (Coffee) context.getBean(ColorCoffee.class);
		coffee.drink();
		if(coffee instanceof Color) {
			Color col = (Color) coffee;
			System.out.println(col.getColor());
		}
	}

결과는 정상적으로 출력됬다

 

 

 

 

 

 

 

 

디버깅으로 빈 객체를 찍어보니 역시나 CGLIB 으로 프록시 객체를 만든것을 확인할 수 있었다

 

 

 

 

 

 

 

 

 

 

 

그런데.. 이렇게 쓰면 코드 가독성이 떨어질것 같은데..

package com.meteor.bbbb;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class ColorCoffeeFix implements Coffee, Color{

	@Autowired
	private Coffee coffee;
	
	@Autowired
	private Color color;
	
	@Override
	public String getColor() {
		return color.getColor();
	}

	@Override
	public void drink() {
		coffee.drink();
	}

}

이렇게 쓰는게 더 좋지 않을까 싶다..

적절한 인트로덕션(introduction) 사용처를 찾아봐야..

댓글()

Spring Event ApplicationEventPublisher

JAVA/Spring|2019. 4. 24. 19:00

Spring에 Event 처리하는 부분이 있길래 의문점이 조금 생겨 코드를 살펴 봤다

물론 동작은 각 설정에 따라 변경될수 있겠지만 기본 설정(가능한 설정을 손 안댄 상태에서)에서 어떻게 동작하는가?

 

"이벤트별 블럭킹 큐에다 넣고 뽑아 가려나?" 그럼 딱히 락처리 할 부분은 없을것 같은데..?

 

코드를 살펴보니 

 

ApplicationEventPublisher에 event들을 발행시, SimpleApplicationEventMulticaster 인경우

TaskExecutor가 지정되어있지 않으면 invokeListener()를 통해 등록된 Listener들을 각각 호출하고 있었다.

 

즉 Listener가 싱글톤인경우 동일 Listener객체에 onApplicationEvent()가 불릴꺼고

싱글톤일 경우 당연히 고려해야 하긴 하겠지만, 여러 스레드가 불릴것은 고려해야 한다.

 

프로토타입의 Publisher 일때는 Listener객체가 싱글톤인 경우 동일 Listener객체가 불리고

 

Listener객체가 프로토타입일 경우 테스트 해보니 invokeListener()시 매번 생성된 Listener객체에 onApplicationEvent() 불리고 있었다.

 

정리

이벤트 발행시에는 붙어있는 이벤트리스너(객체를 재활용하든 매번 만들던)를 직접 부름

 

Publisher : 싱글톤, Listener : 싱글톤

: 여러 스레드가 한 pub 객체를 호출시 동일한 listener의 onApplicationEvent()를 부름

Publisher : 싱글톤, Listener : 프로토타입

: 여러 스레드가 한 pub 객체를 호출시 매번 새로 생성된 listener의 onApplicationEvent()를 부름

Publisher : 프로토타입, Listener : 싱글톤

: 여러 스레드가 여러 pub 객체를 호출시 동일한 listener의 onApplicationEvent()를 부름

Publisher : 프로토타입, Listener : 프로토타입

: 여러 스레드가 여러 pub 객체를 호출시 매번 새로 생성된 listener의 onApplicationEvent()를 부름

 

 

 

 

댓글()

Spring AnnotationConfigApplicationContext

JAVA/Spring|2019. 4. 24. 13:47

이전에는 코드 살펴봤던 내용들을 정리해놓지 않아 어떻게 바뀐건지 파악하기 어려운감이 있어 정리..

 

5.1.6 기준

 

ComponentScan 경로의 클래스들을 조사해서

빈등록 혹은 스캔 대상이 되는 어노테이션이 붙어있는 대상들을 추려서 처리한다 정도겠지만

구체적으로 코드상에서 어떻게 대상들을 조사하는지를 조금 더 살펴보고자 했다.

 

 

 

일단 AnnotationConfigApplicationContext 생성자에서 

AnnotatedBeanDefinitionReader
ClassPathBeanDefinitionScanner

두가지를 생성하고 있다.

 

특히, ClassPathBeanDefinitionScanner  에서 동작을 살펴보니

spring.componentsMETA-INF/spring.components 파일이 컴포넌트 항목들을 캐시하고 있었고

componentsIndex 필드에 담고 있었다.

 

그후 생성자쪽 scan()에서 componentsIndex 필드에 캐시된 항목이 있으면

ClassPathScanningCandidateComponentProvider.addCandidateComponentsFromIndex()를 호출해서 파일에 있는 항목들을 넣고

 

캐시된 항목이 없으면 

ClassPathScanningCandidateComponentProvider.scanCandidateComponents()을 호출해서

컴포넌트 스캔 경로(classpath*:com/meteor/aaaa/**/*.class)에 있는 모든 클래스를 찾아가며

ScannedGenericBeanDefinition 객체를 빈으로 등록한다.

 

일단 빈 스캔과정은 간략하게 살펴봤고..

 

 

요약

1. spring.components 파일에 스캔 항목들 클래스와 붙어있는 어노테이션이 적혀있어 클래스로더를 찾지 않고 활용

2. spring.components 파일(캐시파일)이 없으면 클래스로더에 컴포넌트 스캔 항목 클래스들을 하나씩 분석해서 ScannedGenericBeanDefinition 객체를 만듬

 

다음에 살펴볼 내용은...

"spring.components은 어떤 타이밍에 어떻게 캐시 파일을 생성할까?"

 

 

 

 

댓글()