[gRPC 사용 계기]
MSA(Microservices Architecture)의 관점에서 서버를 분리하여 개발하는 방법을 찾던 중, gRPC는 비교적 러닝 커브가 낮고 개인 개발에서도 실용적인 통신 방식이라 판단하여 이를 사용해보기로 했습니다.
인증 서버는 회원 기능만 담당하고, 그 외 비즈니스 로직은 자원 서버에서 처리하는 구조로 설계하였습니다. 자원 서버로 들어오는 모든 API 요청에 대해 인증 서버로 토큰 검증 요청을 보내고, 그 결과를 바탕으로 인증하는 방식으로 구현하고자 합니다.
이 내용은 인증 서버에 회원 기능이 구현되었다는 전제 하에 작성되었습니다.
[연동 방법 진행 순서]
- 각 build.gradle에 의존성 추가
- 각 application.yml에 설정 추가
- proto 파일 작성
- 자원서버의 gRPC 통신 구현
- 인증서버의 gRPC 통신 구현
- postman으로 통신 테스트
💡 개발 환경
Java 17, Spring 3.3.X, Gradle 8.8, MySQL
1. 각 build.gradle에 의존성 추가
인증서버
> build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
// gRPC통신 시에 사용되는 protobuf 플러그인 설정
id 'com.google.protobuf' version '0.9.4'
}
...
dependencies {
... 그 외 의존성
// gRPC
implementation 'io.grpc:grpc-netty-shaded:1.66.0'
implementation 'io.grpc:grpc-protobuf:1.66.0'
implementation 'io.grpc:grpc-stub:1.66.0'
implementation 'com.google.protobuf:protobuf-java:4.27.4'
implementation 'com.google.protobuf:protobuf-java-util:4.27.4'
implementation 'net.devh:grpc-spring-boot-starter:2.15.0.RELEASE'
// ProtoBuf
compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
}
// gRPC
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.27.4"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:1.66.0"
}
}
generateProtoTasks {
all().each { task ->
task.plugins {
grpc {}
}
}
}
}
// .proto 파일의 위치를 src/main/proto로 지정
sourceSets {
main {
proto {
srcDir 'src/main/proto'
}
}
}
compileJava {
dependsOn 'generateProto'
}
tasks.processResources {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
tasks.named('test') {
useJUnitPlatform()
}
dependencies 블록 설명
- grpc-netty-shaded : gRPC의 서버 및 클라이언트에서 사용되는 Netty 전송 채널 라이브러리
- grpc-protobuf: gRPC와 protobuf를 연결해주는 라이브러리. Protobuf를 gRPC 서비스에서 사용할 수 있게 함.
- grpc-stub: gRPC의 Stub을 사용하여 클라이언트와 서버 간의 원격 호출을 처리.
- protobuf-java: Protobuf 파일에서 생성된 Java 클래스들을 사용하기 위한 라이브러리.
- protobuf-java-util: Protobuf 파일의 JSON 변환을 도와주는 유틸리티 라이브러리.
- grpc-spring-boot-starter: Spring Boot와 gRPC를 쉽게 통합할 수 있도록 도와주는 라이브러리.
- javax.annotation-api: 주석(annotation)을 위한 라이브러리로, 컴파일할 때만 필요하기 때문에 compileOnly로 설정.
자원서버
> build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
// gRPC통신 시에 사용되는 protobuf 플러그인 설정
id 'com.google.protobuf' version '0.9.4'
}
...
dependencies {
... 그 외 의존성
// gRPC
implementation 'io.grpc:grpc-netty-shaded:1.66.0'
implementation 'io.grpc:grpc-protobuf:1.66.0'
implementation 'io.grpc:grpc-stub:1.66.0'
implementation "com.google.protobuf:protobuf-java:4.27.4"
implementation "com.google.protobuf:protobuf-java-util:4.27.4"
// gRPC Client
implementation 'net.devh:grpc-client-spring-boot-starter:3.1.0.RELEASE'
// ProtoBuf
compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
}
// gRPC 및 Protobuf 설정
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.27.4"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:1.66.0"
}
}
generateProtoTasks {
all().each { task ->
task.plugins {
grpc {}
}
}
}
}
// 프로토콜 버퍼 파일 경로 설정
sourceSets {
main {
proto {
srcDir 'src/main/proto'
}
java {
// 프로토콜 파일로부터 생성된 코드 경로를 포함
srcDirs += 'build/generated/source/proto/main/java'
srcDirs += 'build/generated/source/proto/main/grpc'
}
}
}
// 컴파일 태스크가 Protobuf 파일 생성을 포함하도록 의존성 설정
compileJava {
dependsOn 'generateProto'
}
// Protobuf 관련 경로 정리
clean {
delete 'build/generated'
}
dependencies 블록 설명
- grpc-netty-shaded : gRPC의 서버 및 클라이언트에서 사용되는 Netty 전송 채널 라이브러리
- grpc-protobuf: gRPC와 protobuf를 연결해주는 라이브러리. Protobuf를 gRPC 서비스에서 사용할 수 있게 함.
- grpc-stub: gRPC의 Stub을 사용하여 클라이언트와 서버 간의 원격 호출을 처리.
- protobuf-java: Protobuf 파일에서 생성된 Java 클래스들을 사용하기 위한 라이브러리.
- protobuf-java-util: Protobuf 파일의 JSON 변환을 도와주는 유틸리티 라이브러리.
- grpc-client-spring-boot-starter: gRPC 클라이언트를 쉽게 설정하고 Spring Boot에서 사용할 수 있게 해주는 라이브러리
- javax.annotation-api: 주석(annotation)을 위한 라이브러리로, 컴파일할 때만 필요하기 때문에 compileOnly로 설정.
sourceSets 블록 설명
- proto.srcDir 'src/main/proto': Protobuf 파일의 위치를 src/main/proto로 설정.
- java.srcDirs: Protobuf로부터 생성된 Java 코드의 위치를 추가. build/generated/source/proto/main/java는 기본 Protobuf 관련 Java 클래스들이, build/generated/source/proto/main/grpc는 gRPC Stub 코드들이 생성되는 디렉터리.
위와 같이 각각 build.gradle을 작성하고 .proto 파일을 src/main/proto 디렉토리에 두면 별도의 환경 변수 설정 없이도 Gradle 빌드 시 gRPC 관련 코드를 생성하고 컴파일이 진행됨
2. 각 application.yml에 설정 추가
> 인증서버 application.yml
파일의 최상단에 아래와 같이 코드를 입력한다. (local ver.)
# gRPC 인증 서버 포트 설정
grpc:
server:
port: ${GRPC_PORT}
security:
enabled: false
> 자원서버 application.yml
파일의 최상단에 아래와 같이 코드를 입력한다. (local ver.)
# gRPC 자원 서버 포트 설정
grpc:
server:
port: ${GRPC_RESOURCE_SERVER_PORT}
client:
auth:
address: ${GRPC_HOST}:${GRPC_AUTH_SERVER_PORT}
negotiation-type: plaintext
- GRPC_RESOURCE_SERVER_PORT : gRPC의 자원 서버 포트 (실제 백엔드 개발 시에는 활용되지 않는 포트)
- GRPC_AUTH_SERVER_PORT : gRPC의 인증 서버 포트 (인증서버와 통신 시에는 해당 주소로 통신한다.)
- grpc.client.auth : "auth" 클라이언트에 해당하는 서버의 주소 및 통신 방식 등 설정을 지정
3. proto 파일 작성
proto 파일 (proto파일은 인증서버와 자원서버 모두 작성해주어야 함!)
syntax = "proto3";
option java_package = "com.smile.fridaymarket_auth.grpc";
option java_outer_classname = "AuthTokenProto";
service AuthTokenService {
rpc VerifyToken(AuthTokenRequest) returns (AuthTokenResponse);
}
// 요청 객체
message AuthTokenRequest {
string accessToken = 1;
}
// 응답 객체
message AuthTokenResponse {
bool success =1;
string userId = 2;
string username = 3;
}
상세 설명은 아래 더 보기 버튼을 클릭해주세요.
syntax = "proto3";
option java_package = "com.smile.fridaymarket_auth.grpc";
option java_outer_classname = "AuthTokenProto";
- 문법 버전: proto3는 Protocol Buffers의 세 번째 버전을 사용함을 나타냄
- Java 패키지 설정: java_package는 생성된 Java 클래스가 포함될 패키지를 지정하며, com.smile.fridaymarket_auth.grpc로 설정됨
- 클래스 이름 설정: java_outer_classname은 생성될 Java 클래스의 이름을 정의하며, AuthTokenProto로 설정되어 모든 메시지와 서비스를 포함하는 외부 클래스를 생성함
service AuthTokenService {
rpc VerifyToken(AuthTokenRequest) returns (AuthTokenResponse);
}
- gRPC 서비스 정의: AuthTokenService라는 gRPC 서비스를 정의
- RPC 메서드: VerifyToken 메서드는 AuthTokenRequest를 입력으로 받고, AuthTokenResponse를 반환하는 RPC 호출을 정의함
// 요청 객체 message AuthTokenRequest {
string accessToken = 1;
}
- 요청 메시지: AuthTokenRequest 메시지는 인증을 위한 요청 정보를 포함
- 필드: accessToken은 클라이언트가 인증 서버에 전달하는 액세스 토큰을 나타내며, 필드 번호는 1임
// 응답 객체
message AuthTokenResponse {
bool success = 1;
string userId = 2;
string username = 3;
}
- 응답 메시지: AuthTokenResponse 메시지는 인증 결과를 포함.
- 필드:
- success: 인증 성공 여부를 나타내는 boolean 값, 필드 번호 1
- userId: 인증된 사용자의 ID를 포함하는 문자열, 필드 번호 2
- username: 인증된 사용자의 이름을 포함하는 문자열, 필드 번호 3
4. 자원서버의 gRPC 통신 구현
자원서버의 gRPC 통신 서비스 클래스
> GrpcAuthClientService.java
@Slf4j
@Service
public class GrpcAuthClientService {
@GrpcClient("auth")
private AuthTokenServiceGrpc.AuthTokenServiceBlockingStub authStub;
public UserResponse authToken(String accessToken) {
try {
// 인터셉터를 추가한 Stub 생성 (주입된 authStub을 사용)
AuthTokenServiceGrpc.AuthTokenServiceBlockingStub interceptedStub =
authStub.withInterceptors(new JwtClientInterceptor(accessToken));
AuthTokenProto.AuthTokenResponse authTokenResponse = interceptedStub.verifyToken(
AuthTokenProto.AuthTokenRequest.newBuilder()
.setAccessToken(accessToken)
.build()
);
log.info("gRPC 통신 응답 username: {}", authTokenResponse.getUsername());
return new UserResponse(true, authTokenResponse.getUserId(), authTokenResponse.getUsername());
}
catch (StatusRuntimeException e) {
log.info("gRPC 호출 실패 : {}", e.getStatus().getCode().name());
return new UserResponse(false, "Unknown", "Unknown");
}
}
}
- @GrpcClient("auth")
- gRPC 클라이언트 애노테이션: Spring 애플리케이션에서 gRPC 클라이언트를 의존성 주입할 때 사용하는 애노테이션
- 클라이언트 이름: "auth"로 설정되며, application.yml의 grpc.client.auth 설정과 일치해야 함
- 연결 정보: Spring이 gRPC 클라이언트의 연결 정보를 주입하는 역할을 수행
- gRPC Stub: authStub은 인증 서버로 요청을 보낼 때 사용하는 gRPC Stub으로, 토큰 검증 요청을 수행하는 데 사용됨
> JwtClientInterceptor.java
@Slf4j
public class JwtClientInterceptor implements ClientInterceptor {
private static final Metadata.Key<String> AUTHORIZATION_HEADER = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);
private final String accessToken;
public JwtClientInterceptor(String accessToken) {
this.accessToken = accessToken;
}
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> methodDescriptor,
CallOptions callOptions, Channel channel) {
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(channel.newCall(methodDescriptor, callOptions)) {
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
headers.put(AUTHORIZATION_HEADER, "Bearer " + accessToken);
super.start(responseListener, headers);
}
};
}
}
- gRPC 요청을 가로채서 헤더에 JWT 토큰을 추가하는 인터셉터
- Authorization 헤더에 Bearer {토큰} 형식으로 액세스 토큰을 담아 인증 서버로 전달
> GrpcAuthController.java
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class GrpcAuthController { // gRPC 통신 테스트를 위한 클래스
@RequestMapping(value = "", method = RequestMethod.POST)
public UserResponse authToken(HttpServletRequest request) {
UserResponse user = (UserResponse) request.getAttribute("user");
if (user == null || !user.success()) {
throw new CustomException(ErrorCode.TOKEN_NOT_VALID);
}
return user; // 이미 검증된 사용자 정보 반환
}
}
- 자원 서버로 API 호출을 보냈을 때 gRPC 통신이 정상적으로 작동하는지 테스트하기 위해 해당 컨트롤러를 생성하였습니다.
- 요청에서 이미 검증된 사용자 정보를 확인한 후, 그 정보를 반환합니다.
- 만약 인증 정보가 유효하지 않다면 CustomException을 발생시켜 토큰 검증 오류를 처리합니다.
5. 인증서버의 gRPC 통신 구현
> JwtServerInterceptor.java
@RequiredArgsConstructor
@GrpcGlobalServerInterceptor
public class JwtServerInterceptor implements ServerInterceptor {
private final JwtTokenProvider jwtTokenProvider;
// Authorization 헤더 키 생성 (Metadata.Key)
private static final Metadata.Key<String> AUTHORIZATION_KEY =
Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
// Authorization 헤더에서 토큰을 가져옴
String token = headers.get(AUTHORIZATION_KEY);
if (token == null || token.isEmpty()) {
throw new CustomException(ErrorCode.ILLEGAL_ACCESS_TOKEN_NOT_VALID);
}
// 'Bearer ' 접두사 제거
if (token.startsWith("Bearer ")) {
token = token.substring(7); // "Bearer " 뒤의 부분만 남김
}
try {
// 토큰 유효성 검증
Optional<DecodedJWT> decodedJWT = jwtTokenProvider.isValidToken(token);
if (decodedJWT.isEmpty()) {
return closeCallWithError(call, headers, Status.UNAUTHENTICATED, "유효하지 않은 토큰입니다.");
}
} catch (CustomJwtException e) {
return closeCallWithError(call, headers, Status.UNAUTHENTICATED, e.getMessage());
}
// 검증 성공 시, 다음 핸들러 호출
return next.startCall(call, headers);
}
/**
* 인증 실패 시 gRPC 호출을 종료하고 에러 메시지를 반환합니다.
*
* @param call ServerCall 객체
* @param headers gRPC 헤더
* @param status 반환할 gRPC 상태 코드
* @param errorDesc 에러 설명
* @return 빈 ServerCall.Listener 객체
*/
private <ReqT, RespT> ServerCall.Listener<ReqT> closeCallWithError(ServerCall<ReqT, RespT> call, Metadata headers, Status status, String errorDesc) {
log.warn("gRPC 호출 실패: {}", errorDesc);
ErrorResponse errorResponse = new ErrorResponse(false, errorDesc, "BAD_REQUEST");
Metadata trailers = new Metadata();
trailers.put(Key.of("error-details", Metadata.ASCII_STRING_MARSHALLER), toJson(errorResponse));
call.close(status.withDescription(errorDesc), trailers);
return new ServerCall.Listener<>() {
}; // 빈 리스너 반환
}
/**
* ErrorResponse를 JSON 형식으로 변환합니다.
*
* @param errorResponse 에러 응답 객체
* @return JSON 형식의 문자열
*/
private String toJson(ErrorResponse errorResponse) {
try {
return new ObjectMapper().writeValueAsString(errorResponse);
} catch (IOException e) {
log.error("JSON 변환 오류: {}", e.getMessage());
return "{\"success\":false,\"message\":\"서버 오류\",\"httpStatus\":\"INTERNAL_SERVER_ERROR\"}";
}
}
}
- 자원 서버로부터 받은 토큰 값을 gRPC 요청의 헤더에서 꺼내어 검증하는 클래스
- gRPC는 HTTP 통신이 아니며 기본적으로 JSON 형태의 응답을 반환하지 않기 때문에, 예외 발생 시 에러 형태를 통일하기 위해 toJson() 메서드를 생성
> GrpcAuthServerService.java
@Slf4j
@GrpcService
@RequiredArgsConstructor
public class GrpcAuthServerService extends AuthTokenServiceGrpc.AuthTokenServiceImplBase {
private final JwtTokenProvider jwtTokenProvider;
private final UserReader userReader;
@Override
public void verifyToken(AuthTokenProto.AuthTokenRequest request, StreamObserver<AuthTokenProto.AuthTokenResponse> responseObserver) {
String accessToken = request.getAccessToken();
AuthTokenProto.AuthTokenResponse.Builder responseBuilder = AuthTokenProto.AuthTokenResponse.newBuilder();
String username = jwtTokenProvider.getUsernameFromToken(accessToken).replace("\"", "");
try {
User user = userReader.getUserByUsername(username);
UUID userId = user.getId();
responseBuilder
.setUserId(String.valueOf(userId))
.setUsername(username);
responseObserver.onNext(responseBuilder.build());
} catch (CustomException e) {
log.error("사용자 조회 중 예외 발생: {}", e.getMessage());
responseBuilder.setSuccess(false);
responseObserver.onNext(responseBuilder.build());
}
responseObserver.onCompleted();
}
}
- 유효한 액세스 토큰에 대한 인증을 수행하는 서비스 클래스.
- 인증 처리: 클라이언트로부터 액세스 토큰을 받아 해당 토큰의 사용자 정보를 조회하여 응답 빌더 객체에 담아 반환
- 응답 처리: 조회된 사용자 정보를 포함한 응답 객체를 생성하고, 사용자 정보를 반환
- 예외 처리: 사용자 조회 중 예외가 발생할 경우, 응답 객체의 success 필드를 false로 설정하여 오류 정보를 클라이언트에 반환
6. postman으로 통신 테스트
Postman을 사용하여 인증 서버와 자원 서버 간의 gRPC 호출을 테스트합니다.
자원서버로 gRPC가 잘 동작하는 지 postman 테스트를 실시합니다.
헤더에 Authorization의 이름으로 Bearer 형태의 토큰을 담아 요청을 보냅니다.

토큰을 헤더에 담기 위해 interceptor 를 추가하였는데 해당 내용은 다음 포스트에서 좀 더 자세히 작성해보겠습니다!!
이 문서가 gRPC를 사용하여 인증 서버와 자원 서버를 연결하는 데 도움이 되길 바랍니다. (미래의 나에게도 ㅎ..) 추가 질문이나 필요하신 내용이 있다면 언제든지 말씀해 주세요 :)
'Server' 카테고리의 다른 글
HTTPS 설정하기 (0) | 2022.04.28 |
---|
[gRPC 사용 계기]
MSA(Microservices Architecture)의 관점에서 서버를 분리하여 개발하는 방법을 찾던 중, gRPC는 비교적 러닝 커브가 낮고 개인 개발에서도 실용적인 통신 방식이라 판단하여 이를 사용해보기로 했습니다.
인증 서버는 회원 기능만 담당하고, 그 외 비즈니스 로직은 자원 서버에서 처리하는 구조로 설계하였습니다. 자원 서버로 들어오는 모든 API 요청에 대해 인증 서버로 토큰 검증 요청을 보내고, 그 결과를 바탕으로 인증하는 방식으로 구현하고자 합니다.
이 내용은 인증 서버에 회원 기능이 구현되었다는 전제 하에 작성되었습니다.
[연동 방법 진행 순서]
- 각 build.gradle에 의존성 추가
- 각 application.yml에 설정 추가
- proto 파일 작성
- 자원서버의 gRPC 통신 구현
- 인증서버의 gRPC 통신 구현
- postman으로 통신 테스트
💡 개발 환경
Java 17, Spring 3.3.X, Gradle 8.8, MySQL
1. 각 build.gradle에 의존성 추가
인증서버
> build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
// gRPC통신 시에 사용되는 protobuf 플러그인 설정
id 'com.google.protobuf' version '0.9.4'
}
...
dependencies {
... 그 외 의존성
// gRPC
implementation 'io.grpc:grpc-netty-shaded:1.66.0'
implementation 'io.grpc:grpc-protobuf:1.66.0'
implementation 'io.grpc:grpc-stub:1.66.0'
implementation 'com.google.protobuf:protobuf-java:4.27.4'
implementation 'com.google.protobuf:protobuf-java-util:4.27.4'
implementation 'net.devh:grpc-spring-boot-starter:2.15.0.RELEASE'
// ProtoBuf
compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
}
// gRPC
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.27.4"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:1.66.0"
}
}
generateProtoTasks {
all().each { task ->
task.plugins {
grpc {}
}
}
}
}
// .proto 파일의 위치를 src/main/proto로 지정
sourceSets {
main {
proto {
srcDir 'src/main/proto'
}
}
}
compileJava {
dependsOn 'generateProto'
}
tasks.processResources {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
tasks.named('test') {
useJUnitPlatform()
}
dependencies 블록 설명
- grpc-netty-shaded : gRPC의 서버 및 클라이언트에서 사용되는 Netty 전송 채널 라이브러리
- grpc-protobuf: gRPC와 protobuf를 연결해주는 라이브러리. Protobuf를 gRPC 서비스에서 사용할 수 있게 함.
- grpc-stub: gRPC의 Stub을 사용하여 클라이언트와 서버 간의 원격 호출을 처리.
- protobuf-java: Protobuf 파일에서 생성된 Java 클래스들을 사용하기 위한 라이브러리.
- protobuf-java-util: Protobuf 파일의 JSON 변환을 도와주는 유틸리티 라이브러리.
- grpc-spring-boot-starter: Spring Boot와 gRPC를 쉽게 통합할 수 있도록 도와주는 라이브러리.
- javax.annotation-api: 주석(annotation)을 위한 라이브러리로, 컴파일할 때만 필요하기 때문에 compileOnly로 설정.
자원서버
> build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
// gRPC통신 시에 사용되는 protobuf 플러그인 설정
id 'com.google.protobuf' version '0.9.4'
}
...
dependencies {
... 그 외 의존성
// gRPC
implementation 'io.grpc:grpc-netty-shaded:1.66.0'
implementation 'io.grpc:grpc-protobuf:1.66.0'
implementation 'io.grpc:grpc-stub:1.66.0'
implementation "com.google.protobuf:protobuf-java:4.27.4"
implementation "com.google.protobuf:protobuf-java-util:4.27.4"
// gRPC Client
implementation 'net.devh:grpc-client-spring-boot-starter:3.1.0.RELEASE'
// ProtoBuf
compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
}
// gRPC 및 Protobuf 설정
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.27.4"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:1.66.0"
}
}
generateProtoTasks {
all().each { task ->
task.plugins {
grpc {}
}
}
}
}
// 프로토콜 버퍼 파일 경로 설정
sourceSets {
main {
proto {
srcDir 'src/main/proto'
}
java {
// 프로토콜 파일로부터 생성된 코드 경로를 포함
srcDirs += 'build/generated/source/proto/main/java'
srcDirs += 'build/generated/source/proto/main/grpc'
}
}
}
// 컴파일 태스크가 Protobuf 파일 생성을 포함하도록 의존성 설정
compileJava {
dependsOn 'generateProto'
}
// Protobuf 관련 경로 정리
clean {
delete 'build/generated'
}
dependencies 블록 설명
- grpc-netty-shaded : gRPC의 서버 및 클라이언트에서 사용되는 Netty 전송 채널 라이브러리
- grpc-protobuf: gRPC와 protobuf를 연결해주는 라이브러리. Protobuf를 gRPC 서비스에서 사용할 수 있게 함.
- grpc-stub: gRPC의 Stub을 사용하여 클라이언트와 서버 간의 원격 호출을 처리.
- protobuf-java: Protobuf 파일에서 생성된 Java 클래스들을 사용하기 위한 라이브러리.
- protobuf-java-util: Protobuf 파일의 JSON 변환을 도와주는 유틸리티 라이브러리.
- grpc-client-spring-boot-starter: gRPC 클라이언트를 쉽게 설정하고 Spring Boot에서 사용할 수 있게 해주는 라이브러리
- javax.annotation-api: 주석(annotation)을 위한 라이브러리로, 컴파일할 때만 필요하기 때문에 compileOnly로 설정.
sourceSets 블록 설명
- proto.srcDir 'src/main/proto': Protobuf 파일의 위치를 src/main/proto로 설정.
- java.srcDirs: Protobuf로부터 생성된 Java 코드의 위치를 추가. build/generated/source/proto/main/java는 기본 Protobuf 관련 Java 클래스들이, build/generated/source/proto/main/grpc는 gRPC Stub 코드들이 생성되는 디렉터리.
위와 같이 각각 build.gradle을 작성하고 .proto 파일을 src/main/proto 디렉토리에 두면 별도의 환경 변수 설정 없이도 Gradle 빌드 시 gRPC 관련 코드를 생성하고 컴파일이 진행됨
2. 각 application.yml에 설정 추가
> 인증서버 application.yml
파일의 최상단에 아래와 같이 코드를 입력한다. (local ver.)
# gRPC 인증 서버 포트 설정
grpc:
server:
port: ${GRPC_PORT}
security:
enabled: false
> 자원서버 application.yml
파일의 최상단에 아래와 같이 코드를 입력한다. (local ver.)
# gRPC 자원 서버 포트 설정
grpc:
server:
port: ${GRPC_RESOURCE_SERVER_PORT}
client:
auth:
address: ${GRPC_HOST}:${GRPC_AUTH_SERVER_PORT}
negotiation-type: plaintext
- GRPC_RESOURCE_SERVER_PORT : gRPC의 자원 서버 포트 (실제 백엔드 개발 시에는 활용되지 않는 포트)
- GRPC_AUTH_SERVER_PORT : gRPC의 인증 서버 포트 (인증서버와 통신 시에는 해당 주소로 통신한다.)
- grpc.client.auth : "auth" 클라이언트에 해당하는 서버의 주소 및 통신 방식 등 설정을 지정
3. proto 파일 작성
proto 파일 (proto파일은 인증서버와 자원서버 모두 작성해주어야 함!)
syntax = "proto3";
option java_package = "com.smile.fridaymarket_auth.grpc";
option java_outer_classname = "AuthTokenProto";
service AuthTokenService {
rpc VerifyToken(AuthTokenRequest) returns (AuthTokenResponse);
}
// 요청 객체
message AuthTokenRequest {
string accessToken = 1;
}
// 응답 객체
message AuthTokenResponse {
bool success =1;
string userId = 2;
string username = 3;
}
상세 설명은 아래 더 보기 버튼을 클릭해주세요.
syntax = "proto3";
option java_package = "com.smile.fridaymarket_auth.grpc";
option java_outer_classname = "AuthTokenProto";
- 문법 버전: proto3는 Protocol Buffers의 세 번째 버전을 사용함을 나타냄
- Java 패키지 설정: java_package는 생성된 Java 클래스가 포함될 패키지를 지정하며, com.smile.fridaymarket_auth.grpc로 설정됨
- 클래스 이름 설정: java_outer_classname은 생성될 Java 클래스의 이름을 정의하며, AuthTokenProto로 설정되어 모든 메시지와 서비스를 포함하는 외부 클래스를 생성함
service AuthTokenService {
rpc VerifyToken(AuthTokenRequest) returns (AuthTokenResponse);
}
- gRPC 서비스 정의: AuthTokenService라는 gRPC 서비스를 정의
- RPC 메서드: VerifyToken 메서드는 AuthTokenRequest를 입력으로 받고, AuthTokenResponse를 반환하는 RPC 호출을 정의함
// 요청 객체 message AuthTokenRequest {
string accessToken = 1;
}
- 요청 메시지: AuthTokenRequest 메시지는 인증을 위한 요청 정보를 포함
- 필드: accessToken은 클라이언트가 인증 서버에 전달하는 액세스 토큰을 나타내며, 필드 번호는 1임
// 응답 객체
message AuthTokenResponse {
bool success = 1;
string userId = 2;
string username = 3;
}
- 응답 메시지: AuthTokenResponse 메시지는 인증 결과를 포함.
- 필드:
- success: 인증 성공 여부를 나타내는 boolean 값, 필드 번호 1
- userId: 인증된 사용자의 ID를 포함하는 문자열, 필드 번호 2
- username: 인증된 사용자의 이름을 포함하는 문자열, 필드 번호 3
4. 자원서버의 gRPC 통신 구현
자원서버의 gRPC 통신 서비스 클래스
> GrpcAuthClientService.java
@Slf4j
@Service
public class GrpcAuthClientService {
@GrpcClient("auth")
private AuthTokenServiceGrpc.AuthTokenServiceBlockingStub authStub;
public UserResponse authToken(String accessToken) {
try {
// 인터셉터를 추가한 Stub 생성 (주입된 authStub을 사용)
AuthTokenServiceGrpc.AuthTokenServiceBlockingStub interceptedStub =
authStub.withInterceptors(new JwtClientInterceptor(accessToken));
AuthTokenProto.AuthTokenResponse authTokenResponse = interceptedStub.verifyToken(
AuthTokenProto.AuthTokenRequest.newBuilder()
.setAccessToken(accessToken)
.build()
);
log.info("gRPC 통신 응답 username: {}", authTokenResponse.getUsername());
return new UserResponse(true, authTokenResponse.getUserId(), authTokenResponse.getUsername());
}
catch (StatusRuntimeException e) {
log.info("gRPC 호출 실패 : {}", e.getStatus().getCode().name());
return new UserResponse(false, "Unknown", "Unknown");
}
}
}
- @GrpcClient("auth")
- gRPC 클라이언트 애노테이션: Spring 애플리케이션에서 gRPC 클라이언트를 의존성 주입할 때 사용하는 애노테이션
- 클라이언트 이름: "auth"로 설정되며, application.yml의 grpc.client.auth 설정과 일치해야 함
- 연결 정보: Spring이 gRPC 클라이언트의 연결 정보를 주입하는 역할을 수행
- gRPC Stub: authStub은 인증 서버로 요청을 보낼 때 사용하는 gRPC Stub으로, 토큰 검증 요청을 수행하는 데 사용됨
> JwtClientInterceptor.java
@Slf4j
public class JwtClientInterceptor implements ClientInterceptor {
private static final Metadata.Key<String> AUTHORIZATION_HEADER = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);
private final String accessToken;
public JwtClientInterceptor(String accessToken) {
this.accessToken = accessToken;
}
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> methodDescriptor,
CallOptions callOptions, Channel channel) {
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(channel.newCall(methodDescriptor, callOptions)) {
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
headers.put(AUTHORIZATION_HEADER, "Bearer " + accessToken);
super.start(responseListener, headers);
}
};
}
}
- gRPC 요청을 가로채서 헤더에 JWT 토큰을 추가하는 인터셉터
- Authorization 헤더에 Bearer {토큰} 형식으로 액세스 토큰을 담아 인증 서버로 전달
> GrpcAuthController.java
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class GrpcAuthController { // gRPC 통신 테스트를 위한 클래스
@RequestMapping(value = "", method = RequestMethod.POST)
public UserResponse authToken(HttpServletRequest request) {
UserResponse user = (UserResponse) request.getAttribute("user");
if (user == null || !user.success()) {
throw new CustomException(ErrorCode.TOKEN_NOT_VALID);
}
return user; // 이미 검증된 사용자 정보 반환
}
}
- 자원 서버로 API 호출을 보냈을 때 gRPC 통신이 정상적으로 작동하는지 테스트하기 위해 해당 컨트롤러를 생성하였습니다.
- 요청에서 이미 검증된 사용자 정보를 확인한 후, 그 정보를 반환합니다.
- 만약 인증 정보가 유효하지 않다면 CustomException을 발생시켜 토큰 검증 오류를 처리합니다.
5. 인증서버의 gRPC 통신 구현
> JwtServerInterceptor.java
@RequiredArgsConstructor
@GrpcGlobalServerInterceptor
public class JwtServerInterceptor implements ServerInterceptor {
private final JwtTokenProvider jwtTokenProvider;
// Authorization 헤더 키 생성 (Metadata.Key)
private static final Metadata.Key<String> AUTHORIZATION_KEY =
Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
// Authorization 헤더에서 토큰을 가져옴
String token = headers.get(AUTHORIZATION_KEY);
if (token == null || token.isEmpty()) {
throw new CustomException(ErrorCode.ILLEGAL_ACCESS_TOKEN_NOT_VALID);
}
// 'Bearer ' 접두사 제거
if (token.startsWith("Bearer ")) {
token = token.substring(7); // "Bearer " 뒤의 부분만 남김
}
try {
// 토큰 유효성 검증
Optional<DecodedJWT> decodedJWT = jwtTokenProvider.isValidToken(token);
if (decodedJWT.isEmpty()) {
return closeCallWithError(call, headers, Status.UNAUTHENTICATED, "유효하지 않은 토큰입니다.");
}
} catch (CustomJwtException e) {
return closeCallWithError(call, headers, Status.UNAUTHENTICATED, e.getMessage());
}
// 검증 성공 시, 다음 핸들러 호출
return next.startCall(call, headers);
}
/**
* 인증 실패 시 gRPC 호출을 종료하고 에러 메시지를 반환합니다.
*
* @param call ServerCall 객체
* @param headers gRPC 헤더
* @param status 반환할 gRPC 상태 코드
* @param errorDesc 에러 설명
* @return 빈 ServerCall.Listener 객체
*/
private <ReqT, RespT> ServerCall.Listener<ReqT> closeCallWithError(ServerCall<ReqT, RespT> call, Metadata headers, Status status, String errorDesc) {
log.warn("gRPC 호출 실패: {}", errorDesc);
ErrorResponse errorResponse = new ErrorResponse(false, errorDesc, "BAD_REQUEST");
Metadata trailers = new Metadata();
trailers.put(Key.of("error-details", Metadata.ASCII_STRING_MARSHALLER), toJson(errorResponse));
call.close(status.withDescription(errorDesc), trailers);
return new ServerCall.Listener<>() {
}; // 빈 리스너 반환
}
/**
* ErrorResponse를 JSON 형식으로 변환합니다.
*
* @param errorResponse 에러 응답 객체
* @return JSON 형식의 문자열
*/
private String toJson(ErrorResponse errorResponse) {
try {
return new ObjectMapper().writeValueAsString(errorResponse);
} catch (IOException e) {
log.error("JSON 변환 오류: {}", e.getMessage());
return "{\"success\":false,\"message\":\"서버 오류\",\"httpStatus\":\"INTERNAL_SERVER_ERROR\"}";
}
}
}
- 자원 서버로부터 받은 토큰 값을 gRPC 요청의 헤더에서 꺼내어 검증하는 클래스
- gRPC는 HTTP 통신이 아니며 기본적으로 JSON 형태의 응답을 반환하지 않기 때문에, 예외 발생 시 에러 형태를 통일하기 위해 toJson() 메서드를 생성
> GrpcAuthServerService.java
@Slf4j
@GrpcService
@RequiredArgsConstructor
public class GrpcAuthServerService extends AuthTokenServiceGrpc.AuthTokenServiceImplBase {
private final JwtTokenProvider jwtTokenProvider;
private final UserReader userReader;
@Override
public void verifyToken(AuthTokenProto.AuthTokenRequest request, StreamObserver<AuthTokenProto.AuthTokenResponse> responseObserver) {
String accessToken = request.getAccessToken();
AuthTokenProto.AuthTokenResponse.Builder responseBuilder = AuthTokenProto.AuthTokenResponse.newBuilder();
String username = jwtTokenProvider.getUsernameFromToken(accessToken).replace("\"", "");
try {
User user = userReader.getUserByUsername(username);
UUID userId = user.getId();
responseBuilder
.setUserId(String.valueOf(userId))
.setUsername(username);
responseObserver.onNext(responseBuilder.build());
} catch (CustomException e) {
log.error("사용자 조회 중 예외 발생: {}", e.getMessage());
responseBuilder.setSuccess(false);
responseObserver.onNext(responseBuilder.build());
}
responseObserver.onCompleted();
}
}
- 유효한 액세스 토큰에 대한 인증을 수행하는 서비스 클래스.
- 인증 처리: 클라이언트로부터 액세스 토큰을 받아 해당 토큰의 사용자 정보를 조회하여 응답 빌더 객체에 담아 반환
- 응답 처리: 조회된 사용자 정보를 포함한 응답 객체를 생성하고, 사용자 정보를 반환
- 예외 처리: 사용자 조회 중 예외가 발생할 경우, 응답 객체의 success 필드를 false로 설정하여 오류 정보를 클라이언트에 반환
6. postman으로 통신 테스트
Postman을 사용하여 인증 서버와 자원 서버 간의 gRPC 호출을 테스트합니다.
자원서버로 gRPC가 잘 동작하는 지 postman 테스트를 실시합니다.
헤더에 Authorization의 이름으로 Bearer 형태의 토큰을 담아 요청을 보냅니다.

토큰을 헤더에 담기 위해 interceptor 를 추가하였는데 해당 내용은 다음 포스트에서 좀 더 자세히 작성해보겠습니다!!
이 문서가 gRPC를 사용하여 인증 서버와 자원 서버를 연결하는 데 도움이 되길 바랍니다. (미래의 나에게도 ㅎ..) 추가 질문이나 필요하신 내용이 있다면 언제든지 말씀해 주세요 :)
'Server' 카테고리의 다른 글
HTTPS 설정하기 (0) | 2022.04.28 |
---|