spring:
application:
name: eureka-server
server:
port: 190090
eureka:
client:// 클라이언트로써의 역할 / 기본적으로 유레카서버도 자신을 레지스트리에 등록시키기 때문에 작성해야 함
register-with-eureka: false// 다른 eureka 서버에 이 서버를 등록하지 않음(자신이 서버여서 가져올 필요 없음) // -> 유레카 서버에 자신을 등록할지 여부 설정
fetch-registry: false// 다른 eureka 서버의 레지스트리를 가져오지 않음(자신이 서버여서 가져올 필요 없음)// eureka 서버로부터 다른 서비스 인스턴스 목록을 가져오는지 여부
service-url:
defaultZone: http://localhost:190090/eureka// 자신의 호스트주소 설정
server:// 서버로써의 역할
enable-self-preservation: false// 자기 보호모드 비활성화
instance:
hostname: localhost// 호스트 이름 설정//logging:// level:// com.netflix.eureka: OFF// com.netflix.discovery: OFF
// 이 서비스가 EurekaServer임을 선언import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer@SpringBootApplicationpublicclassServerApplication{
publicstaticvoidmain(String[] args){
SpringApplication.run(ServerApplication.class, args);
}
}
spring:
application:
name: service-name // 애플리케이션 이름이 있어야 eureka-server에서 인식하고 등록할 수 있다
server:
port: 190091// 여기까지만으로도 유레카 서버에 등록됨--------------------------------------------------
eureka:
client:
service-url:
defaultZone: http://localhost:190091/eureka/// 유레카 서버 URL
register-with-eureka: true// Eureka 서버에 등록
fetch-registry: true// Eureka 서버로부터 레지스트리 정보 가져오기
instance:
hostname: localhost// 호스트 이름 설정
prefer-ip-address: true// IP 주소 사용 선호
lease-renewal-interval-in-seconds: 30// lease 갱신 간격
lease-expiration-duration-in-seconds: 90// lease 만료 기간
3) Eureka 클라이언트 설정 [ 조회할 서비스 요청 준비 ]
Eureka 서버에서 필요한 서비스의 위치를 조회해야 한다.(RestTemplate 사용하는 방법, FeignClient를 사용하는 방법)
1_ RestTemplate을 사용하는 경우
// @SpringBootApplication이 있는 곳에 @Bean으로 등록한 RestTemplate에 // @LoadBalanced를 붙여 로드밸런싱 기능 추가한다.@SpringBootApplicationpublicclassMyApplication{
publicstaticvoidmain(String[] args){
SpringApplication.run(MyApplication.class, args);
}
@Bean@LoadBalancedpublic RestTemplate restTemplate(){
returnnew RestTemplate();
}
}
// FeignClient에 필요한 서비스 이름을 추가한다// RestTemplate 방식에서 serviceUrl이 여기서 인터페이스로 만들어진다.// 사용하는 url은 상대 서비스에 보낼 것으로 작성하면 됨@FeignClient(name = "my-service")// 애플리케이션 이름publicinterfaceMyServiceClient{
@GetMapping("/api/data")String getData();
}
// FeignClient를 선언하고 상대 서비스를 데이터를 조회해 온다@RestControllerpublicclassMyFeignClientController{
@Autowiredprivate MyServiceClient myServiceClient;
@GetMapping("/get-data-feign")public String getDataWithFeignClient(){
return myServiceClient.getData(); //api가 아닌 메서드로
}
}
- 장애 처리 : 서비스 장애 시 해당 인스턴스를 레지스트리에서 제거하여 해당 서비스로의 다른 서비스 접근을 차단한다
5) Eureka 클라이언트 설정 [ Eureka의 고가용성 구성 ] - 클러스터 구성(역할이 같다는 것으로 등록)
// 유레카 클라이언트를 여러 유레카 서버에 등록
eureka:
client:
service-url:
defaultZone: http://eureka-peer1:8761/eureka/,http://eureka-peer2:8761/eureka/// 유레카 서버 URL을 여러 개로 구성
// FeingClient 선언적 사용 : interface로 상대 서비스 인스턴스 선언// Controller와 같이 선언import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "request-service")publicinterfaceMyServiceClient{
@GetMapping("/endpoint")String getResponse(@RequestParam(name = "param") String param);
}
// FeignClient를 선언하고 상대 서비스를 데이터를 조회 구현@RestController@RequiredArgsConstructorpublicclassMyFeignClientController{
private MyServiceClient myServiceClient;
@GetMapping("/get-data-feign")public String callService(@RequestParam String param){
return myServiceClient.getResponse(param);
}
}
spring:
application:
name: service-name // 애플리케이션 이름이 있어야 eureka-server에서 인식하고 등록할 수 있다
server:
port: 190091
eureka:
client:
service-url:
defaultZone: http://localhost:190091/eureka/// 유레카 서버 URL
register-with-eureka: true// Eureka 서버에 등록
fetch-registry: true// Eureka 서버로부터 레지스트리 정보 가져오기
instance:
hostname: localhost// 호스트 이름 설정
prefer-ip-address: true// IP 주소 사용 선호
lease-renewal-interval-in-seconds: 30// lease 갱신 간격
lease-expiration-duration-in-seconds: 90// lease 만료 기간
request-service:
ribbon:
eureka:
enabled: true//FeignClient에서 제공하는 서비스 인스턴스를 사용하여 로드 밸런싱을 수행
2) 요청받을 서비스 인스턴스 여러 개 등록(Intellij IDE 한정) - Ribbon에서 로드 밸런싱 알고리즘에 맞게 자동 호출
위 사진에서 옵션 수정 클릭 - VM옵션 추가 - "-Dserver.port=19093(실행할 포트)" 입력
FeignClient와 Ribbon 동작 원리
@FeignClient(name="request-service") 어노테이션은 Eureka에 등록된 서비스 이름을 참조
서비스 인스턴스 조회
로드 밸런싱(Ribbon) : 서비스 인스턴스 선택 및 요청 분배
4.Resilience4j 사용하기 [ 서킷 브레이커 ]
프로젝트 생성 - 생성시 dependencies에 resilience4j는 추가하지 않는다
기본 설정(build.gradle) resilience4j를 spring starter에서 추가하지 않고 gradle dependecy에서 추가한다. ( spring starter에서 제공하는 dependency는 구현체가 아니어서 github의 resilience4j로 추가한다.)
// Spring Cloud와 Resilience4j 연동 - Eureka와 Ribbon 등 다른 Spring Cloud 구성요소와 쉽게 통합
spring:
application:
name: my-service
cloud:
circuitbreaker:
resilience4j:
enabled: true
server:
port: 19090
// Resilience4j 설정
resilience4j:
circuitbreaker:
configs:
default: # 기본 구성 이름
registerHealthIndicator: true# 애플리케이션의 헬스 체크에 서킷 브레이커 상태를 추가하여 모니터링 가능# 서킷 브레이커가 동작할 때 사용할 슬라이딩 윈도우의 타입을 설정# COUNT_BASED: 마지막 N번의 호출 결과를 기반으로 상태를 결정# TIME_BASED: 마지막 N초 동안의 호출 결과를 기반으로 상태를 결정
slidingWindowType: COUNT_BASED # 슬라이딩 윈도우의 타입을 호출 수 기반(COUNT_BASED)으로 설정# 슬라이딩 윈도우의 크기를 설정# COUNT_BASED일 경우: 최근 N번의 호출을 저장# TIME_BASED일 경우: 최근 N초 동안의 호출을 저장
slidingWindowSize: 5 # 슬라이딩 윈도우의 크기를 5번의 호출로 설정
minimumNumberOfCalls: 5 # 서킷 브레이커가 동작하기 위해 필요한 최소한의 호출 수를 5로 설정
slowCallRateThreshold: 100 # 느린 호출의 비율이 이 임계값(100%)을 초과하면 서킷 브레이커가 동작
slowCallDurationThreshold: 60000 # 느린 호출의 기준 시간(밀리초)으로, 60초 이상 걸리면 느린 호출로 간주
failureRateThreshold: 50 # 실패율이 이 임계값(50%)을 초과하면 서킷 브레이커가 동작
permittedNumberOfCallsInHalfOpenState: 3 # 서킷 브레이커가 Half-open 상태에서 허용하는 최대 호출 수를 3으로 설정# 서킷 브레이커가 Open 상태에서 Half-open 상태로 전환되기 전에 기다리는 시간
waitDurationInOpenState: 20s # Open 상태에서 Half-open 상태로 전환되기 전에 대기하는 시간을 20초로 설정
// Resilience4j Dashboard를 위한 설정 - http://${hostname}:${port}/actuator/prometheus 에 접속하여 서킷브레이커 항목을 확인 가능
management:
endpoints:
web:
exposure:
include: prometheus
prometheus:
metrics:
export:
enabled: true
Fallback 적용
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
@Service@RequiredArgsConstructorpublicclassProductService{
privatefinal Logger log = LoggerFactory.getLogger(getClass());
privatefinal CircuitBreakerRegistry circuitBreakerRegistry;
@PostConstructpublicvoidregisterEventListener(){
circuitBreakerRegistry.circuitBreaker("productService").getEventPublisher()
.onStateTransition(event -> log.info("#######CircuitBreaker State Transition: {}", event)) // 상태 전환 이벤트 리스너
.onFailureRateExceeded(event -> log.info("#######CircuitBreaker Failure Rate Exceeded: {}", event)) // 실패율 초과 이벤트 리스너
.onCallNotPermitted(event -> log.info("#######CircuitBreaker Call Not Permitted: {}", event)) // 호출 차단 이벤트 리스너
.onError(event -> log.info("#######CircuitBreaker Error: {}", event)); // 오류 발생 이벤트 리스너
}
@CircuitBreaker(name = "productService", fallbackMethod = "fallbackGetProductDetails")public Product getProductDetails(String productId){
log.info("###Fetching product details for productId: {}", productId);
if ("111".equals(productId)) {
log.warn("###Received empty body for productId: {}", productId);
thrownew RuntimeException("Empty response body");
}
returnnew Product(
productId,
"Sample Product"
);
}
public Product fallbackGetProductDetails(String productId, Throwable t){
log.error("####Fallback triggered for productId: {} due to: {}", productId, t.getMessage());
returnnew Product(
productId,
"Fallback Product"
);
}
// 이벤트 설명 표// +---------------------------+-------------------------------------------------+--------------------------------------------+// | 이벤트 | 설명 | 로그 출력 |// +---------------------------+-------------------------------------------------+--------------------------------------------+// | 상태 전환 (Closed -> Open) | 연속된 실패로 인해 서킷 브레이커가 오픈 상태로 전환되면 발생 | CircuitBreaker State Transition: ... |// | 실패율 초과 | 설정된 실패율 임계치를 초과하면 발생 | CircuitBreaker Failure Rate Exceeded: ... |// | 호출 차단 | 서킷 브레이커가 오픈 상태일 때 호출이 차단되면 발생 | CircuitBreaker Call Not Permitted: ... |// | 오류 발생 | 서킷 브레이커 내부에서 호출이 실패하면 발생 | CircuitBreaker Error: ... |// +---------------------------+-------------------------------------------------+--------------------------------------------+// +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+// | 이벤트 | 설명 | 로그 출력 |// +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+// | 메서드 호출 | 제품 정보를 얻기 위해 메서드를 호출 | ###Fetching product details for productId: ... |// | (성공 시) 서킷 브레이커 내부에서 호출 성공 | 메서드 호출이 성공하여 정상적인 응답을 반환 | |// | (실패 시) 서킷 브레이커 내부에서 호출 실패 | 메서드 호출이 실패하여 예외가 발생 | #######CircuitBreaker Error: ... |// | (실패 시) 실패 횟수 증가 | 서킷 브레이커가 실패 횟수를 증가시킴 | |// | (실패율 초과 시) 실패율 초과 | 설정된 실패율 임계치를 초과하면 발생 | #######CircuitBreaker Failure Rate Exceeded: ... |// | (실패율 초과 시) 상태 전환 (Closed -> Open) | 연속된 실패로 인해 서킷 브레이커가 오픈 상태로 전환됨 | #######CircuitBreaker State Transition: Closed -> Open at ... |// | (오픈 상태 시) 호출 차단 | 서킷 브레이커가 오픈 상태일 때 호출이 차단됨 | #######CircuitBreaker Call Not Permitted: ... |// | (오픈 상태 시) 폴백 메서드 호출 | 메서드 호출이 차단될 경우 폴백 메서드 호출 | ####Fallback triggered for productId: ... due to: ... |// +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
}
server:
port: 19091 # 게이트웨이 서비스가 실행될 포트 번호
spring:
main:
web-application-type: reactive # Spring 애플리케이션이 리액티브 웹 애플리케이션으로 설정됨
application:
name: gateway-service # 애플리케이션 이름을 'gateway-service'로 설정
cloud:
gateway:
routes: # Spring Cloud Gateway의 라우팅 설정
- id: order-service # 라우트 식별자
uri: lb://order-service # 'order-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/order/** # /order/** 경로로 들어오는 요청을 이 라우트로 처리
- id: product-service # 라우트 식별자
uri: lb://product-service # 'product-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/product/** # /product/** 경로로 들어오는 요청을 이 라우트로 처리
discovery:
locator:
enabled: true# 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/ # Eureka 서버의 URL을 지정
spring:
application:
name: auth-service
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/
service:
jwt:
access-expiration: 3600000
secret-key: #key값을 임의의 값에 대한 base64로 암호화한 값으로 추가
server:
port: 19095
AuthConfig.java 생성
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration@EnableWebSecuritypublicclassAuthConfig{
// SecurityFilterChain 빈을 정의합니다. 이 메서드는 Spring Security의 보안 필터 체인을 구성합니다.@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http)throws Exception {
http
// CSRF 보호를 비활성화합니다. CSRF 보호는 주로 브라우저 클라이언트를 대상으로 하는 공격을 방지하기 위해 사용됩니다.
.csrf(csrf -> csrf.disable())
// 요청에 대한 접근 권한을 설정합니다.
.authorizeRequests(authorize -> authorize
// /auth/signIn 경로에 대한 접근을 허용합니다. 이 경로는 인증 없이 접근할 수 있습니다.
.requestMatchers("/auth/signIn").permitAll()
// 그 외의 모든 요청은 인증이 필요합니다.
.anyRequest().authenticated()
)
// 세션 관리 정책을 정의합니다. 여기서는 세션을 사용하지 않도록 STATELESS로 설정합니다.
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
// 설정된 보안 필터 체인을 반환합니다.return http.build();
}
}
AuthoService.java 생성
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.util.Date;
@ServicepublicclassAuthService{
@Value("${spring.application.name}")private String issuer;
@Value("${service.jwt.access-expiration}")private Long accessExpiration;
privatefinal SecretKey secretKey;
/**
* AuthService 생성자.
* Base64 URL 인코딩된 비밀 키를 디코딩하여 HMAC-SHA 알고리즘에 적합한 SecretKey 객체를 생성합니다.
*
* @param secretKey Base64 URL 인코딩된 비밀 키
*/publicAuthService(@Value("${service.jwt.secret-key}") String secretKey){
this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
}
/**
* 사용자 ID를 받아 JWT 액세스 토큰을 생성합니다.
*
* @param user_id 사용자 ID
* @return 생성된 JWT 액세스 토큰
*/public String createAccessToken(String user_id){
return Jwts.builder()
// 사용자 ID를 클레임으로 설정
.claim("user_id", user_id)
.claim("role", "ADMIN")
// JWT 발행자를 설정
.issuer(issuer)
// JWT 발행 시간을 현재 시간으로 설정
.issuedAt(new Date(System.currentTimeMillis()))
// JWT 만료 시간을 설정
.expiration(new Date(System.currentTimeMillis() + accessExpiration))
// SecretKey를 사용하여 HMAC-SHA512 알고리즘으로 서명
.signWith(secretKey, io.jsonwebtoken.SignatureAlgorithm.HS512)
// JWT 문자열로 컴팩트하게 변환
.compact();
}
}
6-1.Gateway 추가 [ 보안 구성 ] - 직접 접근하는 곳인 게이트웨이에서 Filter로 추가
build.gradle 설정 추가
implementation 'io.jsonwebtoken:jjwt:0.12.6'
application.yml 설정 추가
server:
port: 19091 # 게이트웨이 서비스가 실행될 포트 번호
spring:
main:
web-application-type: reactive # Spring 애플리케이션이 리액티브 웹 애플리케이션으로 설정됨
application:
name: gateway-service # 애플리케이션 이름을 'gateway-service'로 설정
cloud:
gateway:
routes: # Spring Cloud Gateway의 라우팅 설정
- id: auth-service # 라우트 식별자
uri: lb://auth-service # 'auth-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/auth/signIn # /auth/signIn 경로로 들어오는 요청을 이 라우트로 처리
discovery:
locator:
enabled: true# 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정
service:
jwt:
secret-key: #key값을 임의의 값에 대한 base64로 암호화한 값으로 추가
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @RefreshScope 애노테이션은 Spring 애플리케이션의 빈이 설정 변경을 반영할 수 있도록 하는 역할을 합니다.
* 기본적으로 Spring 애플리케이션의 빈은 애플리케이션이 시작될 때 초기화되고, 설정 값이 변경되더라도 해당 빈은 갱신되지 않습니다.
* 이 애노테이션을 사용하면 /actuator/refresh 엔드포인트를 호출하여 설정 변경 사항을 동적으로 반영할 수 있습니다.
*/@RefreshScope@RestController@RequestMapping("/product")publicclassProductController{
@Value("${server.port}")// 애플리케이션이 실행 중인 포트를 주입받습니다.private String serverPort;
@Value("${message}")private String message;
@GetMappingpublic String getProduct(){
return"Product detail from PORT : " + serverPort + " and message : " + this.message ;
}
}
7-3) 설정 반영하기[ 수동으로 /actuator/refresh 엔드포인트를 호출하는 방법 ]