[내일배움캠프 TIL, Day 6] MSA 실습 1일차: 게이트웨이 보안 심화, Product 도메인 설계
1. 오늘 학습 키워드
- Spring Cloud Gateway: 전역 필터(Global Filter)를 통한 요청/응답 제어 및 로깅
- ServerWebExchange: WebFlux 기반의 비동기 요청/응답 통합 객체 활용
- JWT Filter Refactoring: 인증 검증과 데이터 전달(Mutation)의 책임 분리
- Domain Driven Design (DDD): 도메인 모델에 로직을 응집시키는 객체지향 설계
- Lombok AccessLevel: 캡슐화를 강화하기 위한 접근 제한자 설정
- QueryDSL: 복잡한 조건 검색과 페이징 처리를 위한 동적 쿼리 도구
2. 학습 내용 정리하기
🔐 Gateway & JWT
- ServerWebExchange의 활용:
HttpServletRequest와 달리 비동기 환경에 최적화된 객체로, 요청 정보 조회부터 응답 코드 설정(setStatusCode)까지 한 번에 관리함. - Filter 역할 분리:
- Pre Filter: 요청이 들어오는 즉시 URI를 로깅하고 초기 검사를 수행함.
- Local JWT Filter: 토큰의 유효성을 검사하고,
Claims를 추출함. - Request Mutation: 인증된 사용자 정보(
user_id,role)를 헤더에 담아 뒤쪽 서비스로 전달하기 위해exchange.mutate()를 사용하여 새로운 복사본을 생성함. - Post Filter: 서비스로부터 돌아온 응답이 나갈 때 마지막으로 상태 코드를 로깅함.
@Slf4j
@Component
public class LocalJwtAuthenticationFilter implements GlobalFilter {
@Value("${service.jwt.secret-key}")
private String secretKey;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// ServerWebExchange: Request에 대한 모든 정보와 Response를 처리할 도구를 하나로 묶은 불변 객체. 비동기방식
// 1. 요청 경로 확인
// exchange.getRequest(): 클라이언트가 보낸 URI, 헤더, 쿠키, 파라미터 등을 읽을 수 있다.
String path = exchange.getRequest().getURI().getPath();
// 회원가입, 로그인 -> 필터를 적용하지 않음
if (path.equals("/auth/signIn") || path.equals("/auth/signUp")) {
return chain.filter(exchange); // 응답 스트림을 닫고 프로세스 종료
}
// 2. 토큰 추출 및 검증
String token = extractToken(exchange);
Claims claims = validateToken(token); // 2-1. 검증 및 정보 획득
if (claims == null) { // 2-2. 검증 실패 처리
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 3. 검증 성공 시: 새로운 정보를 담은 exchange로 교체 (Mutate) -> 뒤쪽 서비스(Product, Order 등)로 헤더가 전달됨
ServerWebExchange mutatedExchange = exchange.mutate()
.request(r -> r
.header("X-User-Id", claims.get("user_id").toString())
.header("X-Role", claims.get("role").toString()))
.build();
return chain.filter(mutatedExchange); // 4. 내용을 수정한 복사본(mutatedExchange)을 다음으로 전달
}
// ... (추출 및 검증 로직 생략)
}
@Slf4j
@Component
public class CustomPreFilter implements GlobalFilter, Ordered {
// GlobalFilter: 게이트웨이로 들어오는 모든 요청에 대해 공통적으로 실행되는 필터임을 선언
// Ordered: 여러 필터가 있을 때 실행 순서를 결정
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
log.info("##### Pre Filter: Request URI is " + request.getURI());
// Add any custom logic here
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE; // 가장 높은 우선순위 설정
}
}
@Slf4j
@Component
public class CustomPostFilter implements GlobalFilter, Ordered {
@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();
log.info("##### Post Filter: Response status code is " + response.getStatusCode());
// Add any custom logic here
}));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE; // 가장 낮은 우선순위 설정
}
}
🏗️ 엔티티 설계 : Product Entity 예제
데이터만 담는 바구니가 아니라, 스스로의 상태를 관리하는 풍성한 도메인 모델을 지향함.
- 캡슐화 강화:
@AllArgsConstructor(access = AccessLevel.PROTECTED)와 빌더의PRIVATE설정을 통해 무분별한 객체 생성을 막고 정적 팩토리 메서드(createProduct)로 생성을 일원화함. - 비즈니스 메서드: Setter 대신 명확한 의미를 가진
updateProduct,deleteProduct메서드를 내부에 두어 데이터 일관성을 유지함. - JPA 생명주기 콜백:
@PrePersist,@PreUpdate를 사용하여 생성/수정 시간을 시스템이 자동으로 관리하도록 자동화함.
@Getter
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PROTECTED) // 외부에서 모든 필드 생성자 호출 차단
@Builder(access = AccessLevel.PRIVATE) // 외부에서 빌더 직접 사용 차단.정적 팩토리 메서드 유도
@Table(name = "products")
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer price;
// ... 필드 생략
@PrePersist // JPA 생명주기 콜백: Insert 쿼리 실행 직전 자동실행
protected void onCreate() {
this.createdAt = LocalDateTime.now();
}
public static Product createProduct(ProductRequestDto requestDto, String userId) {
return Product.builder()
.name(requestDto.getName())
.price(requestDto.getPrice())
.createdBy(userId)
.build();
}
}
🔍 QueryDSL 설정
복잡한 검색 조건과 페이징 처리를 위해 도입함.
- JPAQueryFactory:
EntityManager를 주입받아 빈으로 등록하며, 런타임이 아닌 컴파일 시점에 쿼리 오류를 잡아낼 수 있는 안정성을 확보함.
☸️ 인프라의 확장 (Kubernetes)
- 개념: 컨테이너 배포, 확장, 운영을 자동화하는 오케스트레이션 도구.
- Spring Cloud와의 차이:
- Spring Cloud: 서비스 간 통신(Discovery, Config) 등 애플리케이션 내부 로직 중심.
- Kubernetes: 컨테이너 이미지 기반의 배포 관리, 자가 치유(Self-healing) 등 인프라 운영 중심.
- 고민점: 도입 시 확장성과 자동화 측면에서 유리하나, 운영 비용이 발생하고 분산 환경에서의 디버깅이 어려워질 수 있음.
3. 학습하며 겪었던 문제점 & 에러
1) 필터 로그 순서 및 검증 문제
- 상황:
/auth/signIn요청 시 JWT 필터 로그가 찍히지 않음. - 해결: 설계상 로그인 경로는
permitAll처럼 필터를 스킵하도록 구현했기 때문임.Pre Filter->Post Filter순으로 200 OK 로그가 찍힌다면 정문 보안 시스템이 의도대로 작동 중인 것임.
2) Netty MacOS DNS 라이브러리 경고
- 상황: 실행 시
Unable to load netty-resolver-dns-native-macos에러 발생. - 해결: 맥 OS 전용 라이브러리 부재로 인한 경고이며, 시스템 기본값(fallback)으로 자동 전환되므로 로컬 실습에는 지장이 없음을 확인함.
#내일배움캠프 #단기Java #TIL #SpringCloudGateway #JWT인증 #DDD #K8s
Leave a comment