- 정렬 알고리즘
알아보기
'Java Spring 군 > Java' 카테고리의 다른 글
Record 클래스(Java 14이상 적용가능) (1) | 2025.01.15 |
---|---|
Enum (3) | 2024.12.25 |
StringBuilder에 대해 알아보자 - 수정중 (3) | 2024.11.29 |
properties, yml파일 위치 상의 우선순위 - 수정중 (1) | 2024.11.27 |
- 정렬 알고리즘
알아보기
Record 클래스(Java 14이상 적용가능) (1) | 2025.01.15 |
---|---|
Enum (3) | 2024.12.25 |
StringBuilder에 대해 알아보자 - 수정중 (3) | 2024.11.29 |
properties, yml파일 위치 상의 우선순위 - 수정중 (1) | 2024.11.27 |
https://spring.io/microservices
Spring | Microservices
Microservices with Spring BootWith Spring Boot, your microservices can start small and iterate fast. That’s why it has become the de facto standard for Java™ microservices. Quickstart your project with Spring Initializr and then package as a JAR. With
spring.io
Spring | Cloud
We found that the performance of [Spring Cloud Gateway] was very appealing. Low latency, good throughput, [and] a very small percentage of timeouts in our use cases. CHRIS JACKSON, SENIOR DEVELOPER, TD AMERITRADE
spring.io
MSA를 실현시키기 위해 Spring에서 제공하는 framework?
1) Eureka 서버 설정 [ 서비스 레지스트리 역할 등록 ]
build.gradle 에 아래 의존성을 추가해야 한다
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
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
@SpringBootApplication
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
2) Eureka 클라이언트 설정 [ 서비스 등록 ]
build.gradle 에 아래 의존성을 추가해야 한다
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
application.yml에 아래 설정을 추가해야 한다
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를 붙여 로드밸런싱 기능 추가한다.
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
//사용할 Controller에 서비스url을 restTemplate로 불러와서 사용한다.
@RestController
public class MyRestTemplateController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/get-data-rest")
public String getDataWithRestTemplate() {
String serviceUrl = "http://my-service/api/data"; //my-service 위치가 애플리케이션 이름
return restTemplate.getForObject(serviceUrl, String.class);
}
}
2_ FeignClient를 사용하는 경우
// FeignClient를 사용하용하도록 표시해야 한다.
@SpringBootApplication
@EnableFeignClients
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
// FeignClient에 필요한 서비스 이름을 추가한다
// RestTemplate 방식에서 serviceUrl이 여기서 인터페이스로 만들어진다.
// 사용하는 url은 상대 서비스에 보낼 것으로 작성하면 됨
@FeignClient(name = "my-service") // 애플리케이션 이름
public interface MyServiceClient {
@GetMapping("/api/data")
String getData();
}
// FeignClient를 선언하고 상대 서비스를 데이터를 조회해 온다
@RestController
public class MyFeignClientController {
@Autowired
private MyServiceClient myServiceClient;
@GetMapping("/get-data-feign")
public String getDataWithFeignClient() {
return myServiceClient.getData(); //api가 아닌 메서드로
}
}
3_ DiscoveryClient와 RestClient를 활용하는 방법
package com.example.serviceb.controller;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
@RestController
public class ServiceBRestController {
private final DiscoveryClient discoveryClient;
private final RestClient restClient;
public ServiceBRestController(DiscoveryClient discoveryClient, RestClient.Builder restClientBuilder) {
this.discoveryClient = discoveryClient;
restClient = restClientBuilder.build();
}
@GetMapping("helloEureka")
public String helloWorld() {
ServiceInstance serviceInstance = discoveryClient.getInstances("servicea").get(0);
// 인스턴스명이 "servicea"인 인스턴스 중 처음 것을 가져옴
String serviceAResponse = restClient.get()
.uri(serviceInstance.getUri() + "/helloWorld")
.retrieve()
.body(String.class);
return serviceAResponse;
}
}
4) Eureka 서버에서 헬스 체크 및 장애 처리
- 헬스 체크 : 주기적으로 서비스 인스턴스 상태를 확인하여 가용성 확인
- 장애 처리 : 서비스 장애 시 해당 인스턴스를 레지스트리에서 제거하여 해당 서비스로의 다른 서비스 접근을 차단한다
5) Eureka 클라이언트 설정 [ Eureka의 고가용성 구성 ] - 클러스터 구성(역할이 같다는 것으로 등록)
// 유레카 클라이언트를 여러 유레카 서버에 등록
eureka:
client:
service-url:
defaultZone: http://eureka-peer1:8761/eureka/,http://eureka-peer2:8761/eureka/
// 유레카 서버 URL을 여러 개로 구성
유레카 설정값
https://docs.spring.io/spring-cloud-netflix/reference/configprops.html
Configuration Properties :: Spring Cloud Netflix
If this flag is set to {@code true}, {@code ServiceInstanceListSupplier#get(Request request)} method will be implemented to call {@code delegate.get(request)} in classes assignable from {@code DelegatingServiceInstanceListSupplier} that don’t already imp
docs.spring.io
로드 밸런싱
kimyhg.tistory.com
1) Eureka클라이언트 설정 [ FeignClient와 Ribbon 설정 ]
build.gradle 에 의존성을 추가한다.
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' //유레카 클라이언트
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' //FeignClient 추가
//FeignClient 선언적 사용
@SpringBootApplication
@EnableFeignClients
public class MyServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MyServiceApplication.class, args);
}
}
// 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")
public interface MyServiceClient {
@GetMapping("/endpoint")
String getResponse(@RequestParam(name = "param") String param);
}
// FeignClient를 선언하고 상대 서비스를 데이터를 조회 구현
@RestController
@RequiredArgsConstructor
public class MyFeignClientController {
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에서 로드 밸런싱 알고리즘에 맞게 자동 호출
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
implementation 'org.springframework.boot:spring-boot-starter-aop'
//Resilience4j Dashboard 사용하기
implementation 'io.github.resilience4j:resilience4j-micrometer'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
아래는 Resilience4j 를 spring boot에 적용하는 튜토리얼 페이지이다.
https://resilience4j.readme.io/docs/getting-started-3
Getting Started
Getting started with resilience4j-spring-boot2 or resilience4j-spring-boot3
resilience4j.readme.io
// 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
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
@RequiredArgsConstructor
public class ProductService {
private final Logger log = LoggerFactory.getLogger(getClass());
private final CircuitBreakerRegistry circuitBreakerRegistry;
@PostConstruct
public void registerEventListener() {
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);
throw new RuntimeException("Empty response body");
}
return new Product(
productId,
"Sample Product"
);
}
public Product fallbackGetProductDetails(String productId, Throwable t) {
log.error("####Fallback triggered for productId: {} due to: {}", productId, t.getMessage());
return new 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: ... |
// +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
}
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
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을 지정
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.logging.Logger;
@Component
public class CustomPreFilter implements GlobalFilter, Ordered {
private static final Logger logger = Logger.getLogger(CustomPreFilter.class.getName());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest response = exchange.getRequest();
logger.info("Pre Filter: Request URI is " + response.getURI());
// Add any custom logic here
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.logging.Logger;
@Component
public class CustomPostFilter implements GlobalFilter, Ordered {
private static final Logger logger = Logger.getLogger(CustomPostFilter.class.getName());
@Override
public Mono<Void> filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
logger.info("Post Filter: Response status code is " + response.getStatusCode());
// Add any custom logic here
}));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
https://spring.io/guides/gs/gateway
Getting Started | Building a Gateway
As a good developer, we should write some tests to make sure our Gateway is doing what we expect it should. In most cases, we want to limit our dependencies on outside resources, especially in unit tests, so we should not depend on HTTPBin. One solution to
spring.io
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-zuul'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
zuul:
routes:
users-service:
path: /users/**
serviceId: users-service
orders-service:
path: /orders/**
serviceId: orders-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
@Component
public class PreFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 요청 로깅
System.out.println(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
return null;
}
}
implementation 'io.jsonwebtoken:jjwt:0.12.6'
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
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
@EnableWebSecurity
public class AuthConfig {
// SecurityFilterChain 빈을 정의합니다. 이 메서드는 Spring Security의 보안 필터 체인을 구성합니다.
@Bean
public 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();
}
}
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;
@Service
public class AuthService {
@Value("${spring.application.name}")
private String issuer;
@Value("${service.jwt.access-expiration}")
private Long accessExpiration;
private final SecretKey secretKey;
/**
* AuthService 생성자.
* Base64 URL 인코딩된 비밀 키를 디코딩하여 HMAC-SHA 알고리즘에 적합한 SecretKey 객체를 생성합니다.
*
* @param secretKey Base64 URL 인코딩된 비밀 키
*/
public AuthService(@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();
}
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
/**
* 사용자 ID를 받아 JWT 액세스 토큰을 생성하여 응답합니다.
*
* @param user_id 사용자 ID
* @return JWT 액세스 토큰을 포함한 AuthResponse 객체를 반환합니다.
*/
@GetMapping("/auth/signIn")
public ResponseEntity<?> createAuthenticationToken(@RequestParam String user_id){
return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(user_id)));
}
/**
* JWT 액세스 토큰을 포함하는 응답 객체입니다.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
static class AuthResponse {
private String access_token;
}
}
implementation 'io.jsonwebtoken:jjwt:0.12.6'
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 io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.crypto.SecretKey;
@Slf4j
@Component
public class LocalJwtAuthenticationFilter implements GlobalFilter {
@Value("${service.jwt.secret-key}")
private String secretKey;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (path.equals("/auth/signIn")) {
return chain.filter(exchange); // /signIn 경로는 필터를 적용하지 않음
}
String token = extractToken(exchange);
if (token == null || !validateToken(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
private String extractToken(ServerWebExchange exchange) {
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
private boolean validateToken(String token) {
try {
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
Jws<Claims> claimsJws = Jwts.parser()
.verifyWith(key)
.build().parseSignedClaims(token);
log.info("#####payload :: " + claimsJws.getPayload().toString());
// 추가적인 검증 로직 (예: 토큰 만료 여부 확인 등)을 여기에 추가할 수 있습니다.
return true;
} catch (Exception e) {
return false;
}
}
}
6-1) Config 서비스 구성
implementation 'org.springframework.cloud:spring-cloud-config-server'
implementation 'org.springframework.boot:spring-boot-starter-web'
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
server:
port: 18080
spring:
profiles:
active: native
application:
name: config-server
cloud:
config:
server:
#git:
# uri: https://github.com/my-config-repo/config-repo
# clone-on-start: true
native:
search-locations: classpath:/config-repo # 리소스 폴더의 디렉토리 경로
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/
- product-service.yml
server:
port: 19093
message: "product-service message"
- product-service-local.yml
server:
port: 19083
message: "product-service-local message"
7-2) 설정받을 서비스 설정
implementation 'org.springframework.cloud:spring-cloud-starter-config'
spring:
application:
name: my-config-client
profiles:
active: dev
config:
import: "configserver:"
cloud:
config:
discovery:
enabled: true
service-id: config-server
management:
endpoints:
web:
exposure:
include: refresh
message: "default message"
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/
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")
public class ProductController {
@Value("${server.port}") // 애플리케이션이 실행 중인 포트를 주입받습니다.
private String serverPort;
@Value("${message}")
private String message;
@GetMapping
public String getProduct() {
return "Product detail from PORT : " + serverPort + " and message : " + this.message ;
}
}
7-3) 설정 반영하기 [ 수동으로 /actuator/refresh 엔드포인트를 호출하는 방법 ]
로드 밸런싱 - 수정중 (3) | 2024.11.28 |
---|
properties파일과 yml파일 비교(왜 Spring은 properties파일을 default로 생각할까?) (2) | 2024.11.27 |
---|
빌드 자동화 시스템
실행 가능한 jar파일을 만들어준다
Java, Android, and Kotlin, C++ 등 개발자를 위한 빌드도구
- Project
빌드하고 싶은 애플리케이션이나 라이브러리
- Build 스크립트(build.gradle)
프로젝트를 빌드하는 단계를 적은 파일
프로젝트 당 build 스크립트가 한 개 이상
- Dependency Management
프로젝트에 필요한 외부 리소스들을 선언하고 풀어주는 역할
- Tasks
기본 작업 단위 - 코드 컴파일, 테스트 실행
- Plugins
여기에 추가한 내용은 재사용 가능한 빌드 로직을 패키지화하여 task로 제공해 빌드시 사용할 수 있도록 도와준다
group = 'com.sparta' : 아티팩트의 그룹명
version = '0.0.1=SNAPSHOT' : realse될 버전
sourceCompatiblity = '17' : java 소스를 컴파일할 때 사용되는 java 버전
configurations {
compileOnly {
extendsFrom annotationProcessor // compileOnly가 dependency의 anntoationProcessor를 상속하도록 설정
}
smokeTest.extendsFrom testImplementation // smokeTest 에서도 testImplementation을 사용할 수 있다
}
dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'junit:junit:4.13'
smokeTest 'org.apache.httpcomponents:httpclient:4.5.5'
}
repositories{ mavenCentral() } : 프로젝트 빌드에 필요한 의존성을 다운받을 저장소. 여기서는 maven 저장소
mavenCentral은 maven의 중앙저장소로 gradle 내장 함수 mavenCentral()을 이용해 중앙저장소에 있는 의존성들을 가져온다. // 자신의 maven reposity를 구축하여 사설 저장소로도 쓸 수 있다.(maven,ivy를 사용)
repositories{
mavenCentral()
}
# 프로젝트 빌드에 필요한 의존성을 다운받을 저장소. 여기서는 maven 저장소
# mavenCentral은 maven의 중앙저장소로 gradle 내장 함수 mavenCentral()을 이용해
# 중앙저장소에 있는 의존성들을 가져온다. 자신의 maven reposity를 구축하여
# 사설 저장소로도 쓸 수 있다.(maven,ivy를 사용)
Dependency Configuration
apiElements와 runtimeElements는 구성 요소가 라이브러리를 컴파일하거나 실행할 때 사용
- apiElements
- runtimeElements
compileClasspath, runtimeClasspath : 구성 요소 자체를 실행하는 데 필요한 모든 요소를 검색할 때 사용한다.
api, implementation, compileOnly, runtimeOnly : 사용자가 의존성을 선언할 때 사용 / 라이브러리마다 필요한 시점이 다르기 때문에 거기에 맞게 사용
dependencies {
// JWT
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
//json
implementation 'org.json:json:20230227'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
compile project(':sub')
#==> 프로젝트 의존성을 사용하면 해당 프로젝트에서 다른 프로젝트의 의존성을 가져올 수 있다
runtime files('common.jar', 'system.jar')
#==>
#일반 jar파일의 경로를 적어주어 의존성을 추가할 수 있다.
#이렇게 하면 maven 중앙저장소나 사설저장소에 넣지 않고도 의존성을 추가 가능
}
tasks.named('test') {
useJUnitPlatform() // JUnit플랫폼을 이용하여 'test'라는 이름의 task를 추가한다
}
mapper.xml 파일을 resuorces 폴더에 넣고 싶지 않을 때 사용
빌드 시 XML파일을 ClassPath에 포함시켜준
sourceSets {
main {
resources { // 리소스 파일을 찾는다.
srcDirs = ["src/main/java", "src/main/resources"] // 이 경로에서
includes = ["**/*.xml", "**/application.properties"] // 이 유형의 파일을
}
}
}
resources 하위는 package경로가 아닌 directory 경로로 접근하게 된다.
점(.)으로 패키지 간 구분이 안된다.
참조
gradle 파일 이해하기
개요 지금까지 build.gradle 파일을 사용해서 java와 spring 파일들을 빌드했지만, 구조를 정확히 알지 못하는 느낌을 받았다. 그래서 gradle 파일이 어떤식으로 구성되는지 공부해 보려고 한다. settings.g
hyunwook.dev