Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion chat_service/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ out/
### VS Code ###
.vscode/

**/security/
**/security/
73 changes: 29 additions & 44 deletions chat_service/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,58 +8,43 @@ group = 'com.synapse'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
sourceCompatibility = JavaVersion.VERSION_21
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
}

dependencies {
// Spring AI
implementation platform('org.springframework.ai:spring-ai-bom:1.0.0-M4')
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
implementation 'org.springframework.ai:spring-ai-anthropic-spring-boot-starter'
implementation 'org.springframework.ai:spring-ai-vertex-ai-gemini-spring-boot-starter'
// Spring WebFlux
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// Spring Web
implementation 'org.springframework.boot:spring-boot-starter-web'
// WebSocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// OAuth2 Resource Server
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
// WebSocket Security
implementation 'org.springframework.security:spring-security-messaging'
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// H2
runtimeOnly 'com.h2database:h2'
// PostgreSQL
runtimeOnly 'org.postgresql:postgresql'
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// Auth0
implementation 'com.auth0:java-jwt:4.4.0'
//Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testRuntimeOnly 'com.h2database:h2'
subprojects{
apply plugin: 'java'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'org.springframework.boot'

repositories {
mavenCentral()
}

dependencies {
// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
// Spring Web
implementation 'org.springframework.boot:spring-boot-starter-web'
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
}

tasks.named('test') {
useJUnitPlatform()
}

bootJar {
enabled = false
}
37 changes: 37 additions & 0 deletions chat_service/chat_service/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/
58 changes: 58 additions & 0 deletions chat_service/chat_service/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
}

dependencies {
implementation project(':chat_service_api')
// Spring AI
implementation platform('org.springframework.ai:spring-ai-bom:1.0.0-M4')
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
implementation 'org.springframework.ai:spring-ai-anthropic-spring-boot-starter'
implementation 'org.springframework.ai:spring-ai-vertex-ai-gemini-spring-boot-starter'
// Spring WebFlux
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// WebSocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// OAuth2 Resource Server
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
// WebSocket Security
implementation 'org.springframework.security:spring-security-messaging'
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// H2
runtimeOnly 'com.h2database:h2'
// PostgreSQL
runtimeOnly 'org.postgresql:postgresql'
// Auth0
implementation 'com.auth0:java-jwt:4.4.0'
//Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testRuntimeOnly 'com.h2database:h2'
}

def generated = 'src/main/generated'

tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(generated))
}

sourceSets {
main.java.srcDirs += [ generated ]
}

clean {
delete file(generated)
}

tasks.named('test') {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.synapse.chat_service;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableAutoConfiguration
@ComponentScan
@EnableJpaAuditing
public class ChatServiceConfig {

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,20 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Redis 작업에 대한 공통 예외 처리를 위한 어노테이션
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisOperation {

/**
* 작업 설명 (로깅용)
*/
String value() default "";

/**
* 예외 발생 시 기본값 반환 여부
*/
boolean returnDefaultOnError() default false;

/**
* 예외를 다시 던질지 여부
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,52 @@
package com.synapse.chat_service.common.aspect;

import com.synapse.chat_service.common.annotation.RedisOperation;
import com.synapse.chat_service.exception.commonexception.RedisOperationException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import com.synapse.chat_service.common.annotation.RedisOperation;
import com.synapse.chat_service.exception.commonexception.RedisOperationException;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Aspect
@Component
public class RedisOperationAspect {

@Around("@annotation(redisOperation)")
public Object handleRedisOperation(ProceedingJoinPoint joinPoint, RedisOperation redisOperation) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
String operation = redisOperation.value().isEmpty() ? methodName : redisOperation.value();

try {
Object result = joinPoint.proceed();
log.debug("Redis 작업 성공: {}.{}", className, operation);
return result;

} catch (Exception e) {
log.error("Redis 작업 실패: {}.{} - 원인: {}", className, operation, e.getMessage(), e);

if (redisOperation.returnDefaultOnError()) {
log.debug("Redis 작업 실패 시 기본값 반환: {}.{}", className, operation);
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
return getDefaultValue(methodSignature.getReturnType());
}

if (redisOperation.rethrowException()) {
// 예외 체이닝을 통해 원본 예외의 스택 트레이스 보존
String operationDescription = String.format("%s.%s", className, operation);
throw RedisOperationException.operationError(operationDescription, e);
}

log.debug("Redis 작업 실패 시 null 반환: {}.{}", className, operation);
return null;
}
}

private Object getDefaultValue(Class<?> returnType) {
if (returnType == boolean.class || returnType == Boolean.class) {
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.synapse.chat_service.common.util;

import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
Expand All @@ -15,7 +17,7 @@ public class RedisTypeConverter {
/**
* Redis에서 조회한 원시 값을 지정된 타입으로 안전하게 변환
*
* @param rawValue Redis에서 조회한 원시 값
* @param rawValue Redis에서 조회한 원시 값
* @param targetType 변환할 대상 타입
* @return 변환된 객체 (실패 시 null)
*/
Expand All @@ -32,9 +34,9 @@ public <T> T convertValue(Object rawValue, Class<T> targetType) {

// ObjectMapper를 사용한 타입 변환
return objectMapper.convertValue(rawValue, targetType);

} catch (Exception e) {
log.warn("Redis 값 타입 변환 실패: rawValue={}, targetType={}",
log.warn("Redis 값 타입 변환 실패: rawValue={}, targetType={}",
rawValue.getClass().getSimpleName(), targetType.getSimpleName(), e);
return null;
}
Expand All @@ -46,7 +48,7 @@ public <T> T convertValue(Object rawValue, Class<T> targetType) {
public String convertToString(Object rawValue) {
return convertValue(rawValue, String.class);
}

/**
* 객체를 byte 배열로 변환 (Redis 트랜잭션에서 사용)
*
Expand All @@ -57,7 +59,7 @@ public byte[] convertToBytes(Object value) {
if (value == null) {
return new byte[0];
}

try {
return objectMapper.writeValueAsBytes(value);
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,16 @@
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
* 비동기 처리 설정
* AI 모델 호출의 대용량 트래픽을 고려한 스레드 풀 구성
*/
@Configuration
@EnableAsync
public class AsyncConfig {

/**
* AI 서비스 전용 스레드 풀
* 대규모 동시 요청을 처리할 수 있도록 설정
*/
@Bean(name = "aiTaskExecutor")
public Executor aiTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 기본 스레드 수
executor.setMaxPoolSize(50); // 최대 스레드 수
executor.setQueueCapacity(100); // 대기 큐 크기
executor.setCorePoolSize(10); // 기본 스레드 수
executor.setMaxPoolSize(50); // 최대 스레드 수
executor.setQueueCapacity(100); // 대기 큐 크기
executor.setThreadNamePrefix("AI-"); // 스레드 이름 접두사
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,14 @@
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

/**
* ObjectMapper 설정 클래스
*
* 보안 고려사항:
* - Default Typing은 안전하지 않은 역직렬화 취약점을 유발할 수 있어 비활성화
* - 다형성 타입 처리가 필요한 경우 @JsonTypeInfo와 @JsonSubTypes 어노테이션을
* - 해당 클래스에 직접 사용하는 것을 권장
*/
@Configuration
public class ObjectMapperConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

return objectMapper;
}
Expand Down
Loading
Loading