Skip to content
This repository was archived by the owner on Mar 15, 2026. It is now read-only.
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
96 changes: 48 additions & 48 deletions api/cached_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,124 +51,124 @@ func NewCachedSolvedACClient() *CachedSolvedACClient {
}

// Close는 캐시 정리 워커를 중지시킵니다.
func (c *CachedSolvedACClient) Close() {
if c.cleanupCancel != nil {
c.cleanupCancel()
func (cachedClient *CachedSolvedACClient) Close() {
if cachedClient.cleanupCancel != nil {
cachedClient.cleanupCancel()
utils.Info("Cache cleanup worker stopped.")
}
}

// GetUserInfo 캐시를 통해 사용자 정보를 조회합니다
func (c *CachedSolvedACClient) GetUserInfo(ctx context.Context, handle string) (*UserInfo, error) {
atomic.AddInt64(&c.totalCalls, 1)
func (cachedClient *CachedSolvedACClient) GetUserInfo(ctx context.Context, handle string) (*UserInfo, error) {
atomic.AddInt64(&cachedClient.totalCalls, 1)

// 캐시에서 먼저 조회
if cachedData, found := c.cache.GetUserInfo(handle); found {
atomic.AddInt64(&c.cacheHits, 1)
if cachedData, found := cachedClient.cache.GetUserInfo(handle); found {
atomic.AddInt64(&cachedClient.cacheHits, 1)
utils.Debug("Cache hit for user info: %s", handle)
return cachedData.(*UserInfo), nil
}

// 캐시 미스 - API 호출
atomic.AddInt64(&c.cacheMisses, 1)
atomic.AddInt64(&cachedClient.cacheMisses, 1)
utils.Debug("Cache miss for user info: %s, calling API", handle)

userInfo, err := c.client.GetUserInfo(ctx, handle)
userInfo, err := cachedClient.client.GetUserInfo(ctx, handle)
if err != nil {
return nil, err
}

// 성공한 응답을 캐시에 저장
c.cache.SetUserInfo(handle, userInfo)
cachedClient.cache.SetUserInfo(handle, userInfo)

return userInfo, nil
}

// GetUserTop100 캐시를 통해 사용자 TOP 100을 조회합니다
func (c *CachedSolvedACClient) GetUserTop100(ctx context.Context, handle string) (*Top100Response, error) {
atomic.AddInt64(&c.totalCalls, 1)
func (cachedClient *CachedSolvedACClient) GetUserTop100(ctx context.Context, handle string) (*Top100Response, error) {
atomic.AddInt64(&cachedClient.totalCalls, 1)

// 캐시에서 먼저 조회
if cachedData, found := c.cache.GetUserTop100(handle); found {
atomic.AddInt64(&c.cacheHits, 1)
if cachedData, found := cachedClient.cache.GetUserTop100(handle); found {
atomic.AddInt64(&cachedClient.cacheHits, 1)
utils.Debug("Cache hit for user top100: %s", handle)
return cachedData.(*Top100Response), nil
}

// 캐시 미스 - API 호출
atomic.AddInt64(&c.cacheMisses, 1)
atomic.AddInt64(&cachedClient.cacheMisses, 1)
utils.Debug("Cache miss for user top100: %s, calling API", handle)

top100, err := c.client.GetUserTop100(ctx, handle)
top100, err := cachedClient.client.GetUserTop100(ctx, handle)
if err != nil {
return nil, err
}

// 성공한 응답을 캐시에 저장
c.cache.SetUserTop100(handle, top100)
cachedClient.cache.SetUserTop100(handle, top100)

return top100, nil
}

// GetUserAdditionalInfo 캐시를 통해 사용자 추가 정보를 조회합니다
func (c *CachedSolvedACClient) GetUserAdditionalInfo(ctx context.Context, handle string) (*UserAdditionalInfo, error) {
atomic.AddInt64(&c.totalCalls, 1)
func (cachedClient *CachedSolvedACClient) GetUserAdditionalInfo(ctx context.Context, handle string) (*UserAdditionalInfo, error) {
atomic.AddInt64(&cachedClient.totalCalls, 1)

// 캐시에서 먼저 조회
if cachedData, found := c.cache.GetUserAdditionalInfo(handle); found {
atomic.AddInt64(&c.cacheHits, 1)
if cachedData, found := cachedClient.cache.GetUserAdditionalInfo(handle); found {
atomic.AddInt64(&cachedClient.cacheHits, 1)
utils.Debug("Cache hit for user additional info: %s", handle)
return cachedData.(*UserAdditionalInfo), nil
}

// 캐시 미스 - API 호출
atomic.AddInt64(&c.cacheMisses, 1)
atomic.AddInt64(&cachedClient.cacheMisses, 1)
utils.Debug("Cache miss for user additional info: %s, calling API", handle)

additionalInfo, err := c.client.GetUserAdditionalInfo(ctx, handle)
additionalInfo, err := cachedClient.client.GetUserAdditionalInfo(ctx, handle)
if err != nil {
return nil, err
}

// 성공한 응답을 캐시에 저장
c.cache.SetUserAdditionalInfo(handle, additionalInfo)
cachedClient.cache.SetUserAdditionalInfo(handle, additionalInfo)

return additionalInfo, nil
}

// GetUserOrganizations 지정된 사용자의 소속 조직 목록을 가져옵니다 (캐시 포함)
func (c *CachedSolvedACClient) GetUserOrganizations(ctx context.Context, handle string) ([]Organization, error) {
atomic.AddInt64(&c.totalCalls, 1)
func (cachedClient *CachedSolvedACClient) GetUserOrganizations(ctx context.Context, handle string) ([]Organization, error) {
atomic.AddInt64(&cachedClient.totalCalls, 1)

// 캐시에서 먼저 조회
if cachedData, found := c.cache.GetUserOrganizations(handle); found {
atomic.AddInt64(&c.cacheHits, 1)
if cachedData, found := cachedClient.cache.GetUserOrganizations(handle); found {
atomic.AddInt64(&cachedClient.cacheHits, 1)
utils.Debug("Cache hit for user organizations: %s", handle)
return cachedData.([]Organization), nil
}

// 캐시 미스 - API 호출
atomic.AddInt64(&c.cacheMisses, 1)
atomic.AddInt64(&cachedClient.cacheMisses, 1)
utils.Debug("Cache miss for user organizations: %s, calling API", handle)

organizations, err := c.client.GetUserOrganizations(ctx, handle)
organizations, err := cachedClient.client.GetUserOrganizations(ctx, handle)
if err != nil {
return nil, err
}

// 성공한 응답을 캐시에 저장
c.cache.SetUserOrganizations(handle, organizations)
cachedClient.cache.SetUserOrganizations(handle, organizations)

return organizations, nil
}

// GetCacheStats 캐시 통계를 반환합니다
func (c *CachedSolvedACClient) GetCacheStats() CacheMetrics {
cacheStats := c.cache.GetStats()
func (cachedClient *CachedSolvedACClient) GetCacheStats() CacheMetrics {
cacheStats := cachedClient.cache.GetStats()

totalCalls := atomic.LoadInt64(&c.totalCalls)
hits := atomic.LoadInt64(&c.cacheHits)
misses := atomic.LoadInt64(&c.cacheMisses)
totalCalls := atomic.LoadInt64(&cachedClient.totalCalls)
hits := atomic.LoadInt64(&cachedClient.cacheHits)
misses := atomic.LoadInt64(&cachedClient.cacheMisses)

var hitRate float64
if totalCalls > 0 {
Expand Down Expand Up @@ -198,38 +198,38 @@ type CacheMetrics struct {
}

// String CacheMetrics의 문자열 표현을 반환합니다
func (m CacheMetrics) String() string {
func (metrics CacheMetrics) String() string {
return fmt.Sprintf("API Cache Stats: Calls=%d, Hits=%d, Misses=%d, Hit Rate=%.2f%%, Cached Items: UserInfo=%d, Top100=%d, Additional=%d",
m.TotalCalls, m.CacheHits, m.CacheMisses, m.HitRate,
m.UserInfoCached, m.UserTop100Cached, m.UserAdditionalCached)
metrics.TotalCalls, metrics.CacheHits, metrics.CacheMisses, metrics.HitRate,
metrics.UserInfoCached, metrics.UserTop100Cached, metrics.UserAdditionalCached)
}

// ClearCache 모든 캐시를 삭제합니다
func (c *CachedSolvedACClient) ClearCache() {
c.cache.Clear()
atomic.StoreInt64(&c.cacheHits, 0)
atomic.StoreInt64(&c.cacheMisses, 0)
atomic.StoreInt64(&c.totalCalls, 0)
func (cachedClient *CachedSolvedACClient) ClearCache() {
cachedClient.cache.Clear()
atomic.StoreInt64(&cachedClient.cacheHits, 0)
atomic.StoreInt64(&cachedClient.cacheMisses, 0)
atomic.StoreInt64(&cachedClient.totalCalls, 0)
utils.Info("API cache cleared")
}

// WarmupCache 주요 참가자들에 대한 캐시를 미리 로드합니다
func (c *CachedSolvedACClient) WarmupCache(handles []string) error {
func (cachedClient *CachedSolvedACClient) WarmupCache(handles []string) error {
utils.Info("Starting cache warmup for %d users", len(handles))

for _, handle := range handles {
// 이미 캐시에 있다면 스킵
if _, found := c.cache.GetUserInfo(handle); found {
if _, found := cachedClient.cache.GetUserInfo(handle); found {
continue
}

// 백그라운드에서 데이터 로드
go func(h string) {
ctx := context.Background()
if _, err := c.GetUserInfo(ctx, h); err != nil {
if _, err := cachedClient.GetUserInfo(ctx, h); err != nil {
utils.Warn("Cache warmup failed for user info %s: %v", h, err)
}
if _, err := c.GetUserTop100(ctx, h); err != nil {
if _, err := cachedClient.GetUserTop100(ctx, h); err != nil {
utils.Warn("Cache warmup failed for top100 %s: %v", h, err)
}
}(handle)
Expand Down
44 changes: 22 additions & 22 deletions api/solvedac.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,17 @@ func NewSolvedACClient() *SolvedACClient {
}

// GetUserInfo 지정된 핸들의 사용자 정보를 가져옵니다
func (c *SolvedACClient) GetUserInfo(ctx context.Context, handle string) (*UserInfo, error) {
func (client *SolvedACClient) GetUserInfo(ctx context.Context, handle string) (*UserInfo, error) {
if !utils.IsValidBaekjoonID(handle) {
return nil, fmt.Errorf("잘못된 핸들 형식: %s", handle)
}

url := fmt.Sprintf("%s/user/show?handle=%s", c.baseURL, handle)
return c.getUserInfoWithRetry(ctx, url, handle)
url := fmt.Sprintf("%s/user/show?handle=%s", client.baseURL, handle)
return client.getUserInfoWithRetry(ctx, url, handle)
}

// doRequest 공통 HTTP 요청 및 재시도 로직
func (c *SolvedACClient) doRequest(ctx context.Context, url, requestType, handle string) ([]byte, error) {
func (client *SolvedACClient) doRequest(ctx context.Context, url, requestType, handle string) ([]byte, error) {
var lastErr error

for attempt := 0; attempt < constants.MaxRetries; attempt++ {
Expand All @@ -110,7 +110,7 @@ func (c *SolvedACClient) doRequest(ctx context.Context, url, requestType, handle
continue
}

resp, err := c.client.Do(req)
resp, err := client.client.Do(req)
if err != nil {
lastErr = fmt.Errorf("%s 조회 실패: %w", requestType, err)
utils.Warn("Attempt %d failed for %s %s: %v", attempt+1, requestType, handle, err)
Expand Down Expand Up @@ -149,8 +149,8 @@ func (c *SolvedACClient) doRequest(ctx context.Context, url, requestType, handle
}

// 재시도 로직을 포함한 사용자 정보 조회
func (c *SolvedACClient) getUserInfoWithRetry(ctx context.Context, url, handle string) (*UserInfo, error) {
body, err := c.doRequest(ctx, url, "user info", handle)
func (client *SolvedACClient) getUserInfoWithRetry(ctx context.Context, url, handle string) (*UserInfo, error) {
body, err := client.doRequest(ctx, url, "user info", handle)
if err != nil {
return nil, err
}
Expand All @@ -167,18 +167,18 @@ func (c *SolvedACClient) getUserInfoWithRetry(ctx context.Context, url, handle s
}

// GetUserTop100 지정된 사용자의 TOP 100 문제를 가져옵니다
func (c *SolvedACClient) GetUserTop100(ctx context.Context, handle string) (*Top100Response, error) {
func (client *SolvedACClient) GetUserTop100(ctx context.Context, handle string) (*Top100Response, error) {
if !utils.IsValidBaekjoonID(handle) {
return nil, fmt.Errorf("잘못된 핸들 형식: %s", handle)
}

url := fmt.Sprintf("%s/user/top_100?handle=%s", c.baseURL, handle)
return c.getUserTop100WithRetry(ctx, url, handle)
url := fmt.Sprintf("%s/user/top_100?handle=%s", client.baseURL, handle)
return client.getUserTop100WithRetry(ctx, url, handle)
}

// 재시도 로직을 포함한 TOP 100 조회
func (c *SolvedACClient) getUserTop100WithRetry(ctx context.Context, url, handle string) (*Top100Response, error) {
body, err := c.doRequest(ctx, url, "top 100", handle)
func (client *SolvedACClient) getUserTop100WithRetry(ctx context.Context, url, handle string) (*Top100Response, error) {
body, err := client.doRequest(ctx, url, "top 100", handle)
if err != nil {
return nil, err
}
Expand All @@ -194,18 +194,18 @@ func (c *SolvedACClient) getUserTop100WithRetry(ctx context.Context, url, handle
}

// GetUserAdditionalInfo 지정된 사용자의 추가 정보를 가져옵니다
func (c *SolvedACClient) GetUserAdditionalInfo(ctx context.Context, handle string) (*UserAdditionalInfo, error) {
func (client *SolvedACClient) GetUserAdditionalInfo(ctx context.Context, handle string) (*UserAdditionalInfo, error) {
if !utils.IsValidBaekjoonID(handle) {
return nil, fmt.Errorf("잘못된 핸들 형식: %s", handle)
}

url := fmt.Sprintf("%s/user/additional_info?handle=%s", c.baseURL, handle)
return c.getUserAdditionalInfoWithRetry(ctx, url, handle)
url := fmt.Sprintf("%s/user/additional_info?handle=%s", client.baseURL, handle)
return client.getUserAdditionalInfoWithRetry(ctx, url, handle)
}

// 재시도 로직을 포함한 사용자 추가 정보 조회
func (c *SolvedACClient) getUserAdditionalInfoWithRetry(ctx context.Context, url, handle string) (*UserAdditionalInfo, error) {
body, err := c.doRequest(ctx, url, "additional info", handle)
func (client *SolvedACClient) getUserAdditionalInfoWithRetry(ctx context.Context, url, handle string) (*UserAdditionalInfo, error) {
body, err := client.doRequest(ctx, url, "additional info", handle)
if err != nil {
return nil, err
}
Expand All @@ -229,18 +229,18 @@ func (c *SolvedACClient) getUserAdditionalInfoWithRetry(ctx context.Context, url
}

// GetUserOrganizations 지정된 사용자의 소속 조직 목록을 가져옵니다
func (c *SolvedACClient) GetUserOrganizations(ctx context.Context, handle string) ([]Organization, error) {
func (client *SolvedACClient) GetUserOrganizations(ctx context.Context, handle string) ([]Organization, error) {
if !utils.IsValidBaekjoonID(handle) {
return nil, fmt.Errorf("잘못된 핸들 형식: %s", handle)
}

url := fmt.Sprintf("%s/user/organizations?handle=%s", c.baseURL, handle)
return c.getUserOrganizationsWithRetry(ctx, url, handle)
url := fmt.Sprintf("%s/user/organizations?handle=%s", client.baseURL, handle)
return client.getUserOrganizationsWithRetry(ctx, url, handle)
}

// 재시도 로직을 포함한 사용자 조직 목록 조회
func (c *SolvedACClient) getUserOrganizationsWithRetry(ctx context.Context, url, handle string) ([]Organization, error) {
body, err := c.doRequest(ctx, url, "user organizations", handle)
func (client *SolvedACClient) getUserOrganizationsWithRetry(ctx context.Context, url, handle string) ([]Organization, error) {
body, err := client.doRequest(ctx, url, "user organizations", handle)
if err != nil {
return nil, err
}
Expand Down
Loading
Loading