
https://school.programmers.co.kr/learn/courses/30/lessons/86491
— 문제 설명
명함 지갑을 만드는 회사에서 지갑의 크기를 정하려고 합니다. 다양한 모양과 크기의 명함들을 모두 수납할 수 있으면서, 작아서 들고 다니기 편한 지갑을 만들어야 합니다. 이러한 요건을 만족하는 지갑을 만들기 위해 디자인팀은 모든 명함의 가로 길이와 세로 길이를 조사했습니다.
아래 표는 4가지 명함의 가로 길이와 세로 길이를 나타냅니다.
| 명함 번호 | 가로 길이 | 세로 길이 |
|---|---|---|
| 1 | 60 | 50 |
| 2 | 30 | 70 |
| 3 | 60 | 30 |
| 4 | 80 | 40 |
가장 긴 가로 길이와 세로 길이가 각각 80, 70이기 때문에 80(가로) x 70(세로) 크기의 지갑을 만들면 모든 명함들을 수납할 수 있습니다. 하지만 2번 명함을 가로로 눕혀 수납한다면 80(가로) x 50(세로) 크기의 지갑으로 모든 명함들을 수납할 수 있습니다. 이때의 지갑 크기는 4000(=80 x 50)입니다.
모든 명함의 가로 길이와 세로 길이를 나타내는 2차원 배열 sizes가 매개변수로 주어집니다. 모든 명함을 수납할 수 있는 가장 작은 지갑을 만들 때, 지갑의 크기를 return 하도록 solution 함수를 완성해주세요.
— 제한 조건
— 입출력 예
| sizes | result |
|---|---|
| [[60, 50], [30, 70], [60, 30], [80, 40]] | 4000 |
| [[10, 7], [12, 3], [8, 15], [14, 7], [5, 15]] | 120 |
| [[14, 4], [19, 6], [6, 16], [18, 7], [7, 11]] | 133 |
입출력 예 #1
문제 예시와 같습니다.
입출력 예 #2
명함들을 적절히 회전시켜 겹쳤을 때, 3번째 명함(가로: 8, 세로: 15)이 다른 모든 명함보다 크기가 큽니다. 따라서 지갑의 크기는 3번째 명함의 크기와 같으며, 120(=8 x 15)을 return 합니다.
입출력 예 #3
명함들을 적절히 회전시켜 겹쳤을 때, 모든 명함을 포함하는 가장 작은 지갑의 크기는 133(=19 x 7)입니다.
— 문제 풀이
import java.util.*;
class Solution {
public int solution(int[][] sizes) {
int w = 0;
int h = 0;
for(int i=0;i<sizes.length;i++){
w = Math.max(w, Math.max(sizes[i][0],sizes[i][1]));
h = Math.max(h, Math.min(sizes[i][0],sizes[i][1]));
}
return w*h;
}
}
https://school.programmers.co.kr/learn/courses/30/lessons/12926
— 문제 설명
어떤 문장의 각 알파벳을 일정한 거리만큼 밀어서 다른 알파벳으로 바꾸는 암호화 방식을 시저 암호라고 합니다. 예를 들어 "AB"는 1만큼 밀면 "BC"가 되고, 3만큼 밀면 "DE"가 됩니다. "z"는 1만큼 밀면 "a"가 됩니다. 문자열 s와 거리 n을 입력받아 s를 n만큼 민 암호문을 만드는 함수, solution을 완성해 보세요.
— 제한 조건
— 입출력 예
| s | n | result |
|---|---|---|
| "AB" | 1 | "BC" |
| "z" | 1 | "a" |
| "a B z" | 4 | "e F d" |
— 문제 풀이
class Solution {
public String solution(String s, int n) {
int a = (int)'a';
int A = (int)'A';
for(int i=0;i<s.length();i++){
if(s.charAt(i)==' ')continue;
int cur = (int)s.charAt(i);
if(cur>=a){
cur = ((cur-a)+n)%26 + a;
}else {
cur = ((cur-A)+n)%26 + A;
}
s = s.substring(0,i) + (char)cur + s.substring(i+1);
}
String answer = s;
return answer;
}
}
org.springframework.cloud:spring-cloud-starter-netflix-eureka-serverspring.application.name=server
server.port=19090
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.instance.hostname=localhost
eureka.client.service-url.defaultZone=http://localhost:19090/eureka/org.springframework.cloud:spring-cloud-starter-netflix-eureka-clientorg.springframework.boot:spring-boot-starter-webspring.application.name=first
server.port=19091
eureka.client.service-url.defaultZone=http://localhost:19090/eureka/spring.application.name=second
server.port=19092
eureka.client.service-url.defaultZone=http://localhost:19090/eureka/
org.springframework.cloud:spring-cloud-starter-openfeign@FeignClient(name = "my-service")
public interface MyServiceClient {
@GetMapping("/endpoint")
String getResponse(@RequestParam(name = "param") String param);
}eureka.client.service-url.defaultZone=http://localhost:19090/eureka/org.springframework.cloud:spring-cloud-starter-netflix-eureka-clientorg.springframework.cloud:spring-cloud-starter-openfeignorg.springframework.boot:spring-boot-starter-web@EnableFeignClients - SpringBootApplication에 추가spring:
application:
name: order-service
server:
port: 19091
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/
spring:
application:
name: product-service
server:
port: 19092
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/


Modify options - Add VM options 으로 VM option 추가
-Dserver.port={인스턴스 port} 입력
Product Controller 작성 ( Product Project )
@RestController
public class ProductController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
return "Product " + id + "info!!! From Port : " + serverPort;
}
}
@FeignClient(name = "product-service")
public interface ProductClient {
@GetMapping("/product/{id}")
String getProduct(@PathVariable String id);
}@Service
@RequiredArgsConstructor
public class OrderService {
private final ProductClient productClient;
public String getOrderInfo(String orderId) {
if(orderId.equals("1")) {
String productId = "2";
String productInfo = productClient.getProduct(productId);
return "Your order is " + orderId + ". And " + productInfo;
}
return "Not exist order...";
}
}@RestController
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@GetMapping("/order/{orderId}")
public String order(@PathVariable("orderId") String orderId) {
return orderService.getOrderInfo(orderId);
}
}Eureka Server 실행
Order Server 실행
Product 19092~19094 서버 실행
Eureka 접속 화면


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초로 설정@Service
public class MyService {
@CircuitBreaker(name = "myService", fallbackMethod = "fallbackMethod")
public String myMethod() {
// 외부 서비스 호출
return externalService.call();
}
public String fallbackMethod(Throwable t) {
return "Fallback response";
}
}io.github.resilience4j:resilience4j-micrometerio.micrometer:micrometer-registry-prometheusorg.springframework.boot:spring-boot-starter-actuatormanagement:
endpoints:
web:
exposure:
include: prometheus
prometheus:
metrics:
export:
enabled: trueio.github.resilience4j:resilience4j-spring-boot3:2.2.0org.springframework.boot:spring-boot-starter-aopspring:
application:
name: sample
server:
port: 19090
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초로 설정
management:
endpoints:
web:
exposure:
include: prometheus
prometheus:
metrics:
export:
enabled: true@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
private String id;
private String title;
}@Service
@RequiredArgsConstructor
public class ProductService {
private final Logger log = Logger.getLogger(this.getClass().getName());
@CircuitBreaker(name = "productService", fallbackMethod = "fallbackGetProductDetail")
public Product getProductDetail(String productId) {
if ("111".equals(productId)) {
throw new RuntimeException("Empty Response Body");
}
return new Product(productId, "Sample Product : " + productId);
}
public Product fallbackGetProductDetail(String productId, Throwable t) {
return new Product("0", "Fallback Product : " + productId);
}
}@RestController
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable String id) {
return productService.getProductDetail(id);
}
}


@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 = "fallbackGetProductDetail")
public Product getProductDetail(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 fallbackGetProductDetail(String productId, Throwable t) {
log.error("####Fallback triggered for productId: {} due to: {}", productId, t.getMessage());
return new Product(
productId,
"Fallback Product"
);
}
}2024-08-01T16:59:48.516+09:00 INFO 2860 --- [sample] [io-19090-exec-5] c.s.resilience4j.sample.ProductService : ###Fetching product details for productId: 111
2024-08-01T16:59:48.516+09:00 WARN 2860 --- [sample] [io-19090-exec-5] c.s.resilience4j.sample.ProductService : ###Received empty body for productId: 111
2024-08-01T16:59:48.516+09:00 INFO 2860 --- [sample] [io-19090-exec-5] c.s.resilience4j.sample.ProductService : #######CircuitBreaker Error: 2024-08-01T16:59:48.516828300+09:00[Asia/Seoul]: CircuitBreaker 'productService' recorded an error: 'java.lang.RuntimeException: Empty response body'. Elapsed time: 0 ms
2024-08-01T16:59:48.516+09:00 INFO 2860 --- [sample] [io-19090-exec-5] c.s.resilience4j.sample.ProductService : #######CircuitBreaker Failure Rate Exceeded: 2024-08-01T16:59:48.516828300+09:00[Asia/Seoul]: CircuitBreaker 'productService' exceeded failure rate threshold. Current failure rate: 60.0
2024-08-01T16:59:48.519+09:00 INFO 2860 --- [sample] [io-19090-exec-5] c.s.resilience4j.sample.ProductService : #######CircuitBreaker State Transition: 2024-08-01T16:59:48.519958500+09:00[Asia/Seoul]: CircuitBreaker 'productService' changed state from CLOSED to OPEN
2024-08-01T16:59:48.519+09:00 ERROR 2860 --- [sample] [io-19090-exec-5] c.s.resilience4j.sample.ProductService : ####Fallback triggered for productId: 111 due to: Empty response bodyorg.springframework.boot:spring-boot-starter-weborg.springframework.boot:spring-boot-starter-actuatororg.springframework.cloud:spring-cloud-starter-gatewayorg.springframework.cloud:spring-cloud-starter-netflix-eureka-client
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정
routes:
- id: users-service # 라우트 식별자
uri: lb://users-service # 'users-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/users/** # /users/** 경로로 들어오는 요청을 이 라우트로 처리
- id: orders-service # 라우트 식별자
uri: lb://orders-service # 'orders-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/orders/** #/orders/** 경로로 들어오는 요청을 이 라우트로 처리
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/@Component
public class PreFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 요청 로깅
System.out.println("Request: " + exchange.getRequest().getPath());
return chain.filter(exchange);
}
@Override
public int getOrder() { // 필터의 순서를 지정합니다.
return -1; // 필터 순서를 가장 높은 우선 순위로 설정합니다.
}
}@Component
public class PostFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 응답 로깅
System.out.println("Response Status: " + exchange.getResponse().getStatusCode());
}));
}
@Override
public int getOrder() {
return -1;
}
}Eureka Server와 Order, Product 서버 생성은 FeignClient 실습 참고 !!!
- Eureka Server 19090
- Cloud Gateway 19091
- Order 인스턴스 1개 19092
- Product 인스턴스 2개 19093, 19094
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을 지정@Component
public class CustomPreFilter implements GlobalFilter, Ordered {
private static final Logger loggger = Logger.getLogger(CustomPreFilter.class.getName());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
loggger.info("Pre Filter: Request URI : " + request.getURI());
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}@Component
public class CustomPostFilter implements GlobalFilter, Ordered {
private static final Logger logger = Logger.getLogger(CustomPostFilter.class.getName());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
logger.info("Post Filter : Response status code is " + response.getStatusCode());
}));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
dependencies {
implementation 'io.jsonwebtoken:jjwt:0.12.6'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
compileOnly 'org.projectlombok:lombok'
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'
testImplementation 'io.projectreactor:reactor-test'
}spring:
application:
name: auth-service
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/
service:
jwt:
access-expiration: 3600000
secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
server:
port: 19095@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();
}
}@Service
public class AuthService {
@Value("${spring.application.name}")
private String issuer;
@Value("${service.jwt.access-expiration}")
private Long accessExpiration;
private final SecretKey secretKey;
public AuthService(@Value("${service.jwt.secret-key}") String secretKey) {
this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
}
public String createAccessToken(String user_id){
return Jwts.builder()
.claim("user_id", user_id)
.issuer(issuer)
.issuedAt(Date.from(Instant.now()))
.expiration(Date.from(Instant.now().plusMillis(accessExpiration)))
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}
}@RestController
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@GetMapping("/auth/signIn")
public ResponseEntity<?> createAuthToken(@RequestParam String user_id){
return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(user_id)));
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class AuthResponse {
private String access_token;
}
}
- id: auth-service # 라우트 식별자
uri: lb://auth-service # 'auth-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/auth/signIn # /auth/signIn 경로로 들어오는 요청을 이 라우트로 처리@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);
}
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;
}
}
}
@Getter
@Builder
public class Response<T> {
private String status;
private String message;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T data;
private LocalDateTime timestamp;
}public class ResponseUtil {
public static <T> Response<T> success(String message, T data) {
return new Response<>(200,message,data, LocalDateTime.now());
}
public static <T> Response<T> error(String message, int status) {
return new Response<>(status,message, LocalDateTime.now());
}
}public ResponseEntity<?> getUserInfo(String userId) {
User user = userRepository.findOneByUserId(userId).orElse(null);
return ResponseEntity.ok(ResponseUtil.success("회원 정보 조회 성공", user.toUserDto()));
}.requestMatchers(HttpMethod.POST, "/user").permitAll()
정말 많은 것을 학습해 주신게 TIL 에 잘 드러나네요 수고하셨습니다👍🏻