Skip to content
Open
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
3 changes: 3 additions & 0 deletions motionit/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ out/
### VS Code ###
.vscode/

### log ###
/logs

### custom ###
db_dev.mv.db
db_dev.trace.db
Expand Down
4 changes: 4 additions & 0 deletions motionit/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ dependencies {

// OpenAI (GPT)
implementation("com.theokanning.openai-gpt3-java:service:0.18.2")

// actuator, micrometer
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-prometheus")
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,10 @@ public YoutubeVideoMetadata fetchMetedata(String youtubeUrl) {
// YouTube Data API 호출 URL 구성
String url = YOUTUBE_API_URL + "?id=" + videoId + "&part=snippet,contentDetails&key=" + apiKey;

System.out.println("🔥 [YouTube API 호출 URL] " + url);

// TODO: timeout 설정 등 추가 구성 필요
RestTemplate restTemplate = new RestTemplate();
Map response = restTemplate.getForObject(url, Map.class);

System.out.println("🔥 [YouTube API 응답] " + response);

// 응답에서 필요한 데이터 추출
List<Map> items = (List<Map>)response.get("items");
if (items == null || items.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package com.back.motionit.global.init;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import com.back.motionit.domain.challenge.mission.entity.ChallengeMissionStatus;
import com.back.motionit.domain.challenge.mission.repository.ChallengeMissionStatusRepository;
import com.back.motionit.domain.challenge.participant.entity.ChallengeParticipant;
import com.back.motionit.domain.challenge.participant.entity.ChallengeParticipantRole;
import com.back.motionit.domain.challenge.participant.repository.ChallengeParticipantRepository;
import com.back.motionit.domain.challenge.room.entity.ChallengeRoom;
import com.back.motionit.domain.challenge.room.repository.ChallengeRoomRepository;
import com.back.motionit.domain.challenge.video.entity.ChallengeVideo;
import com.back.motionit.domain.challenge.video.entity.OpenStatus;
import com.back.motionit.domain.challenge.video.repository.ChallengeVideoRepository;
import com.back.motionit.domain.user.entity.LoginType;
import com.back.motionit.domain.user.entity.User;
import com.back.motionit.domain.user.repository.UserRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
@Profile("perf") // ✅ perf 프로파일에서만 작동
@RequiredArgsConstructor
public class PerfDataInitializer {
private final UserRepository userRepository;
private final ChallengeRoomRepository challengeRoomRepository;
private final ChallengeParticipantRepository challengeParticipantRepository;
private final ChallengeVideoRepository challengeVideoRepository;
private final ChallengeMissionStatusRepository challengeMissionStatusRepository;

@Bean
public ApplicationRunner initPerfDummyData() {
return args -> {
if (challengeRoomRepository.count() > 0) {
log.info("✅ perf 데이터 이미 존재. 초기화 스킵");
return;
}

log.info("🚀 perf 프로파일용 더미 데이터 생성 시작");

// 1️⃣ 유저 생성
User host = userRepository.save(User.builder()
.kakaoId(9001L)
.email("[email protected]")
.nickname("PerfHost")
.password("1234")
.loginType(LoginType.KAKAO)
.userProfile("https://picsum.photos/100?perf1")
.build());

List<User> users = IntStream.range(1, 11)
.mapToObj(i -> userRepository.save(User.builder()
.kakaoId(9100L + i)
.email("perf_user" + i + "@example.com")
.nickname("PerfUser" + i)
.password("pass" + i)
.loginType(LoginType.KAKAO)
.userProfile("https://picsum.photos/100?perf" + (i + 1))
.build()))
.collect(Collectors.toList());

// 2️⃣ 챌린지 방 생성
ChallengeRoom room = challengeRoomRepository.save(new ChallengeRoom(
host,
"🔥 K6 부하테스트 전용 방",
"부하테스트용 방입니다.",
50,
OpenStatus.OPEN,
LocalDateTime.now().minusDays(1),
LocalDateTime.now().plusDays(7),
"images/test/perf_room.png",
null,
new ArrayList<>(),
new ArrayList<>()
));

// 3️⃣ 참가자 생성
ChallengeParticipant hostParticipant = challengeParticipantRepository.save(
ChallengeParticipant.builder()
.user(host)
.challengeRoom(room)
.role(ChallengeParticipantRole.HOST)
.quited(false)
.challengeStatus(false)
.build()
);

List<ChallengeParticipant> participants = users.stream()
.map(u -> ChallengeParticipant.builder()
.user(u)
.challengeRoom(room)
.role(ChallengeParticipantRole.NORMAL)
.quited(false)
.challengeStatus(false)
.build())
.collect(Collectors.toList());
challengeParticipantRepository.saveAll(participants);

// 4️⃣ 오늘의 영상 생성
ChallengeVideo todayVideo = challengeVideoRepository.save(ChallengeVideo.builder()
.challengeRoom(room)
.user(host)
.youtubeVideoId("yt_perf_today")
.title("오늘의 퍼포먼스 테스트 영상")
.thumbnailUrl("https://img.youtube.com/vi/yt_perf_today/0.jpg")
.duration(300)
.uploadDate(LocalDate.now())
.isTodayMission(true)
.build());

// 5️⃣ 미션 상태
List<ChallengeMissionStatus> missions = new ArrayList<>();
missions.add(new ChallengeMissionStatus(hostParticipant, LocalDate.now()));

for (ChallengeParticipant p : participants) {
missions.add(new ChallengeMissionStatus(p, LocalDate.now()));
}
challengeMissionStatusRepository.saveAll(missions);

log.info("🎯 perf 더미데이터 생성 완료! [roomId={}, users={}, videoId={}]",
room.getId(), users.size(), todayVideo.getId());
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.back.motionit.global.logging;

import java.util.concurrent.TimeUnit;

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 io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class PerformanceMonitoringAspect {

private final MeterRegistry meterRegistry;

@Around("execution(* com.back.motionit.domain.challenge..controller..*(..)) || "
+ "execution(* com.back.motionit.domain.challenge..service..*(..)) || "
+ "execution(* com.back.motionit.domain.challenge..repository..*(..))")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
Object result = joinPoint.proceed();
long durationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);

MethodSignature signature = (MethodSignature)joinPoint.getSignature();
String className = signature.getDeclaringType().getSimpleName();
String methodName = signature.getName();
String layer = getLayer(className);

Timer timer = Timer.builder("method.execution.time")
.description("Method execution time in milliseconds")
.tags("layer", layer, "class", className, "method", methodName)
.register(meterRegistry);
timer.record(durationMs, TimeUnit.MILLISECONDS);

if (log.isInfoEnabled()) {
log.info("[PERF][{}] {}.{} executed in {} ms", layer, className, methodName, durationMs);
}
return result;
}

private String getLayer(String className) {
if (className.toLowerCase().contains("controller")) {
return "controller";
}
if (className.toLowerCase().contains("service")) {
return "service";
}
if (className.toLowerCase().contains("repository")) {
return "repository";
}
return "other";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,25 @@ private void authenticate(HttpServletRequest request, HttpServletResponse respon
return;
}

String accessToken = requestContext.getCookieValue("accessToken", "");
if (jwtTokenProvider.isExpired(accessToken)) {
throw new BusinessException(AuthErrorCode.TOKEN_EXPIRED);
}

// Authorization 헤더 확인
String headerAuthorization = requestContext.getHeader("Authorization", "");
String accessToken = null;

if (!headerAuthorization.isBlank()) {
if (!headerAuthorization.startsWith(BEARER_PREFIX)) {
throw new BusinessException(AuthErrorCode.AUTH_HEADER_INVALID_SCHEME);
}
if (!headerAuthorization.isBlank() && headerAuthorization.startsWith(BEARER_PREFIX)) {
accessToken = headerAuthorization.substring(BEARER_PREFIX.length());
} else {
// 헤더 없으면 쿠키에서 accessToken 가져오기 (기존 로직 그대로)
accessToken = requestContext.getCookieValue("accessToken", "");
}

// 토큰 존재 여부 확인
if (accessToken == null || accessToken.isBlank()) {
throw new BusinessException(AuthErrorCode.TOKEN_INVALID);
}
Comment on lines +84 to +87
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gksdud1109 이 부분은 아래쪽에 payload에서 예외처리가 되어 있고, 재발급 로직 고려하면 TOKEN_EXPIRED 예외처리보다 먼저 이루어지면 안되서 아예 삭제해주시면 됩니다!


// 만료 확인
if (jwtTokenProvider.isExpired(accessToken)) {
throw new BusinessException(AuthErrorCode.TOKEN_EXPIRED);
}

Map<String, Object> payload = socialAuthService.payloadOrNull(accessToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/api/v1/storage/**").permitAll()
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/ws/**").permitAll()
.requestMatchers(
"/actuator/health",
"/actuator/metrics/**",
"/actuator/prometheus"
).permitAll() // 모니터링/Actuator 관련
.anyRequest().authenticated())
.csrf((csrf) -> csrf.disable())
.headers((headers) -> headers
Expand Down
9 changes: 9 additions & 0 deletions perf/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 환경 변수 파일
.env

# k6 실행 로그
logs/

# IDE 캐시 / 임시파일
.DS_Store
*.log
55 changes: 55 additions & 0 deletions perf/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
version: "3.8"

services:
prometheus:
image: prom/prometheus:latest
container_name: perf_prometheus
ports:
- "9090:9090"
command:
- --web.enable-remote-write-receiver
- --enable-feature=native-histograms
- --config.file=/etc/prometheus/prometheus.yml
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
networks:
- monitoring

grafana:
image: grafana/grafana:latest
container_name: perf_grafana
ports:
- "3300:3000"
volumes:
- ./grafana-provisioning:/etc/grafana/provisioning
- ./grafana-dashboard:/var/lib/grafana/dashboards
depends_on:
- prometheus
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
networks:
- monitoring

k6:
image: grafana/k6:latest
container_name: perf_k6
volumes:
- ./k6-scripts:/scripts
environment:
- JWT_SECRET=${JWT_SECRET}
- K6_PROMETHEUS_RW_SERVER_URL=http://prometheus:9090/api/v1/write
- K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM=true
command: run -o experimental-prometheus-rw /scripts/loadtest.js
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- prometheus
networks:
- monitoring

networks:
monitoring:
driver: bridge
Loading