From 510377376991a30300ffb98c617230aa5fc87825 Mon Sep 17 00:00:00 2001 From: kubrickcode Date: Thu, 27 Nov 2025 12:51:55 +0000 Subject: [PATCH] =?UTF-8?q?refactor(go):=20API=20=ED=81=B4=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EC=96=B8=ED=8A=B8=20DI=20=ED=8C=A8=ED=84=B4=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 매 API 호출마다 새로운 HTTP 클라이언트를 생성하던 구조를 개선하여 클라이언트를 재사용할 수 있도록 DI 패턴 적용 - APIClient 인터페이스 정의로 테스트 시 mock 주입 가능 - 각 Scraper 생성자가 client를 주입받도록 변경 fix #258 --- src/go/apps/auction-item-stat-scraper/main.go | 4 +- .../scraper/scraper.go | 11 +- .../apps/market-item-category-scraper/main.go | 4 +- .../scraper/scraper.go | 17 +- .../scraper/scraper_test.go | 197 ++++++++++++++++++ src/go/apps/market-item-scraper/main.go | 4 +- .../market-item-scraper/scraper/scraper.go | 12 +- src/go/apps/market-item-stat-scraper/main.go | 4 +- .../scraper/scraper.go | 13 +- src/go/libs/loa-api/request/client.go | 20 ++ .../loa-api/request/get-auction-item-list.go | 9 +- .../libs/loa-api/request/get-category-list.go | 11 +- .../loa-api/request/get-market-item-list.go | 9 +- .../libs/loa-api/request/get-market-item.go | 9 +- 14 files changed, 279 insertions(+), 45 deletions(-) create mode 100644 src/go/apps/market-item-category-scraper/scraper/scraper_test.go create mode 100644 src/go/libs/loa-api/request/client.go diff --git a/src/go/apps/auction-item-stat-scraper/main.go b/src/go/apps/auction-item-stat-scraper/main.go index fd0dba7f..7c15a1ca 100644 --- a/src/go/apps/auction-item-stat-scraper/main.go +++ b/src/go/apps/auction-item-stat-scraper/main.go @@ -8,6 +8,7 @@ import ( "github.com/KubrickCode/loa-work/src/go/apps/auction-item-stat-scraper/converter" "github.com/KubrickCode/loa-work/src/go/apps/auction-item-stat-scraper/scraper" "github.com/KubrickCode/loa-work/src/go/libs/env" + "github.com/KubrickCode/loa-work/src/go/libs/loaApi/request" "github.com/KubrickCode/loa-work/src/go/libs/loadb" "github.com/KubrickCode/loa-work/src/go/libs/monitoring" "github.com/KubrickCode/loa-work/src/go/libs/schedule" @@ -27,7 +28,8 @@ func main() { log.Fatal(err) } - scraper := scraper.NewScraper(db) + client := request.NewClient() + scraper := scraper.NewScraper(client, db) converter := converter.NewConverter(db) combinedTask := func() error { diff --git a/src/go/apps/auction-item-stat-scraper/scraper/scraper.go b/src/go/apps/auction-item-stat-scraper/scraper/scraper.go index c8f5a3c2..5700ca17 100644 --- a/src/go/apps/auction-item-stat-scraper/scraper/scraper.go +++ b/src/go/apps/auction-item-stat-scraper/scraper/scraper.go @@ -14,12 +14,17 @@ import ( ) type Scraper struct { + client request.APIClient db loadb.DB rateLimiter *rate.Limiter } -func NewScraper(db loadb.DB) *Scraper { - return &Scraper{db: db, rateLimiter: rate.NewLimiter(rate.Every(time.Second), 1)} +func NewScraper(client request.APIClient, db loadb.DB) *Scraper { + return &Scraper{ + client: client, + db: db, + rateLimiter: rate.NewLimiter(rate.Every(time.Second), 1), + } } func (s *Scraper) Start() error { @@ -67,7 +72,7 @@ func (s *Scraper) getItemStatsToCreate(category *models.AuctionItemCategory, ite return nil, fmt.Errorf("rate limiter error: %w", err) } - auctionItemListResp, err := request.GetAuctionItemList(&loaApi.GetAuctionItemListParams{ + auctionItemListResp, err := s.client.GetAuctionItemList(&loaApi.GetAuctionItemListParams{ CategoryCode: category.Code, ItemName: item.Name, PageNo: 1, diff --git a/src/go/apps/market-item-category-scraper/main.go b/src/go/apps/market-item-category-scraper/main.go index badbc2fa..01cc6f05 100644 --- a/src/go/apps/market-item-category-scraper/main.go +++ b/src/go/apps/market-item-category-scraper/main.go @@ -4,6 +4,7 @@ import ( "log" "github.com/KubrickCode/loa-work/src/go/apps/market-item-category-scraper/scraper" + "github.com/KubrickCode/loa-work/src/go/libs/loaApi/request" "github.com/KubrickCode/loa-work/src/go/libs/loadb" ) @@ -13,7 +14,8 @@ func main() { log.Fatal(err) } - scraper := scraper.NewScraper(db) + client := request.NewClient() + scraper := scraper.NewScraper(client, db) err = scraper.Start() if err != nil { diff --git a/src/go/apps/market-item-category-scraper/scraper/scraper.go b/src/go/apps/market-item-category-scraper/scraper/scraper.go index 03028852..870428c6 100644 --- a/src/go/apps/market-item-category-scraper/scraper/scraper.go +++ b/src/go/apps/market-item-category-scraper/scraper/scraper.go @@ -1,6 +1,7 @@ package scraper import ( + "errors" "fmt" "log" @@ -9,12 +10,18 @@ import ( "github.com/KubrickCode/loa-work/src/go/libs/loadb/models" ) +var ErrNoMarketItemCategories = errors.New("no market item categories found") + type Scraper struct { - db loadb.DB + client request.APIClient + db loadb.DB } -func NewScraper(db loadb.DB) *Scraper { - return &Scraper{db: db} +func NewScraper(client request.APIClient, db loadb.DB) *Scraper { + return &Scraper{ + client: client, + db: db, + } } func (s *Scraper) Start() error { @@ -34,7 +41,7 @@ func (s *Scraper) Start() error { } func (s *Scraper) getCategories() ([]*models.MarketItemCategory, error) { - resp, err := request.GetCategoryList() + resp, err := s.client.GetCategoryList() if err != nil { return nil, err } @@ -42,7 +49,7 @@ func (s *Scraper) getCategories() ([]*models.MarketItemCategory, error) { categories := GetFlattenCategories(resp.Categories) if len(categories) == 0 { - return nil, fmt.Errorf("no market item categories found") + return nil, ErrNoMarketItemCategories } return categories, nil diff --git a/src/go/apps/market-item-category-scraper/scraper/scraper_test.go b/src/go/apps/market-item-category-scraper/scraper/scraper_test.go new file mode 100644 index 00000000..5fe99e80 --- /dev/null +++ b/src/go/apps/market-item-category-scraper/scraper/scraper_test.go @@ -0,0 +1,197 @@ +package scraper + +import ( + "errors" + "testing" + + "github.com/KubrickCode/loa-work/src/go/libs/loaApi" +) + +type mockAPIClient struct { + getAuctionItemListFunc func(params *loaApi.GetAuctionItemListParams) (*loaApi.GetAuctionItemListResponse, error) + getCategoryListFunc func() (*loaApi.GetCategoryListResponse, error) + getMarketItemFunc func(params *loaApi.GetMarketItemParams) (*loaApi.GetMarketItemResponse, error) + getMarketItemListFunc func(params *loaApi.GetMarketItemListParams) (*loaApi.GetMarketItemListResponse, error) +} + +func (m *mockAPIClient) GetAuctionItemList(params *loaApi.GetAuctionItemListParams) (*loaApi.GetAuctionItemListResponse, error) { + if m.getAuctionItemListFunc != nil { + return m.getAuctionItemListFunc(params) + } + return nil, errors.New("not implemented") +} + +func (m *mockAPIClient) GetCategoryList() (*loaApi.GetCategoryListResponse, error) { + if m.getCategoryListFunc != nil { + return m.getCategoryListFunc() + } + return nil, errors.New("not implemented") +} + +func (m *mockAPIClient) GetMarketItem(params *loaApi.GetMarketItemParams) (*loaApi.GetMarketItemResponse, error) { + if m.getMarketItemFunc != nil { + return m.getMarketItemFunc(params) + } + return nil, errors.New("not implemented") +} + +func (m *mockAPIClient) GetMarketItemList(params *loaApi.GetMarketItemListParams) (*loaApi.GetMarketItemListResponse, error) { + if m.getMarketItemListFunc != nil { + return m.getMarketItemListFunc(params) + } + return nil, errors.New("not implemented") +} + +func TestGetCategories_Success(t *testing.T) { + mockClient := &mockAPIClient{ + getCategoryListFunc: func() (*loaApi.GetCategoryListResponse, error) { + return &loaApi.GetCategoryListResponse{ + Categories: []loaApi.Category{ + { + Code: 10000, + CodeName: "Test Category 1", + Subs: []loaApi.SubCategory{ + {Code: 10001, CodeName: "Test Sub Category 1"}, + }, + }, + { + Code: 20000, + CodeName: "Test Category 2", + Subs: []loaApi.SubCategory{}, + }, + }, + }, nil + }, + } + + scraper := &Scraper{ + client: mockClient, + db: nil, + } + + categories, err := scraper.getCategories() + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if len(categories) != 3 { + t.Errorf("Expected 3 categories (2 parent + 1 sub), got %d", len(categories)) + } + + expectedCodes := []int{10000, 10001, 20000} + for i, category := range categories { + if category.Code != expectedCodes[i] { + t.Errorf("Expected category code %d, got %d", expectedCodes[i], category.Code) + } + } +} + +func TestGetCategories_APIError(t *testing.T) { + expectedErr := errors.New("API connection failed") + mockClient := &mockAPIClient{ + getCategoryListFunc: func() (*loaApi.GetCategoryListResponse, error) { + return nil, expectedErr + }, + } + + scraper := &Scraper{ + client: mockClient, + db: nil, + } + + _, err := scraper.getCategories() + if err == nil { + t.Fatal("Expected error, got nil") + } + + if err != expectedErr { + t.Errorf("Expected error %v, got %v", expectedErr, err) + } +} + +func TestGetCategories_EmptyResponse(t *testing.T) { + mockClient := &mockAPIClient{ + getCategoryListFunc: func() (*loaApi.GetCategoryListResponse, error) { + return &loaApi.GetCategoryListResponse{ + Categories: []loaApi.Category{}, + }, nil + }, + } + + scraper := &Scraper{ + client: mockClient, + db: nil, + } + + _, err := scraper.getCategories() + if err == nil { + t.Fatal("Expected error for empty categories, got nil") + } + + if !errors.Is(err, ErrNoMarketItemCategories) { + t.Errorf("Expected ErrNoMarketItemCategories, got %v", err) + } +} + +func TestGetFlattenCategories(t *testing.T) { + categories := []loaApi.Category{ + { + Code: 10000, + CodeName: "Parent 1", + Subs: []loaApi.SubCategory{ + {Code: 10001, CodeName: "Child 1-1"}, + {Code: 10002, CodeName: "Child 1-2"}, + }, + }, + { + Code: 20000, + CodeName: "Parent 2", + Subs: []loaApi.SubCategory{ + {Code: 20001, CodeName: "Child 2-1"}, + }, + }, + } + + flattened := GetFlattenCategories(categories) + + if len(flattened) != 5 { + t.Errorf("Expected 5 flattened categories, got %d", len(flattened)) + } + + expectedCodes := []int{10000, 10001, 10002, 20000, 20001} + for i, category := range flattened { + if category.Code != expectedCodes[i] { + t.Errorf("At index %d: expected code %d, got %d", i, expectedCodes[i], category.Code) + } + } + + for _, category := range flattened { + if category.ID != 0 { + t.Errorf("Expected ID to be 0 (new record), got %d", category.ID) + } + } +} + +func TestGetFlattenCategories_EmptySubCategories(t *testing.T) { + categories := []loaApi.Category{ + { + Code: 10000, + CodeName: "Category without subs", + Subs: []loaApi.SubCategory{}, + }, + } + + flattened := GetFlattenCategories(categories) + + if len(flattened) != 1 { + t.Errorf("Expected 1 category, got %d", len(flattened)) + } + + if flattened[0].Code != 10000 { + t.Errorf("Expected code 10000, got %d", flattened[0].Code) + } + + if flattened[0].Name != "Category without subs" { + t.Errorf("Expected name 'Category without subs', got %s", flattened[0].Name) + } +} diff --git a/src/go/apps/market-item-scraper/main.go b/src/go/apps/market-item-scraper/main.go index c00bfdf8..f96029fc 100644 --- a/src/go/apps/market-item-scraper/main.go +++ b/src/go/apps/market-item-scraper/main.go @@ -4,6 +4,7 @@ import ( "log" "github.com/KubrickCode/loa-work/src/go/apps/market-item-scraper/scraper" + "github.com/KubrickCode/loa-work/src/go/libs/loaApi/request" "github.com/KubrickCode/loa-work/src/go/libs/loadb" ) @@ -13,7 +14,8 @@ func main() { log.Fatal(err) } - scraper := scraper.NewScraper(db) + client := request.NewClient() + scraper := scraper.NewScraper(client, db) err = scraper.Start() if err != nil { diff --git a/src/go/apps/market-item-scraper/scraper/scraper.go b/src/go/apps/market-item-scraper/scraper/scraper.go index 019a900f..ed99e134 100644 --- a/src/go/apps/market-item-scraper/scraper/scraper.go +++ b/src/go/apps/market-item-scraper/scraper/scraper.go @@ -11,11 +11,15 @@ import ( ) type Scraper struct { - db loadb.DB + client request.APIClient + db loadb.DB } -func NewScraper(db loadb.DB) *Scraper { - return &Scraper{db: db} +func NewScraper(client request.APIClient, db loadb.DB) *Scraper { + return &Scraper{ + client: client, + db: db, + } } func (s *Scraper) Start() error { @@ -58,7 +62,7 @@ func (s *Scraper) getItemsToSave(categories []*models.MarketItemCategory) ([]*mo pageNo := 1 for { - resp, err := request.GetMarketItemList(&loaApi.GetMarketItemListParams{ + resp, err := s.client.GetMarketItemList(&loaApi.GetMarketItemListParams{ CategoryCode: category.Code, PageNo: pageNo, }) diff --git a/src/go/apps/market-item-stat-scraper/main.go b/src/go/apps/market-item-stat-scraper/main.go index 6d08dcf4..567113c6 100644 --- a/src/go/apps/market-item-stat-scraper/main.go +++ b/src/go/apps/market-item-stat-scraper/main.go @@ -8,6 +8,7 @@ import ( "github.com/KubrickCode/loa-work/src/go/apps/market-item-stat-scraper/converter" "github.com/KubrickCode/loa-work/src/go/apps/market-item-stat-scraper/scraper" "github.com/KubrickCode/loa-work/src/go/libs/env" + "github.com/KubrickCode/loa-work/src/go/libs/loaApi/request" "github.com/KubrickCode/loa-work/src/go/libs/loadb" "github.com/KubrickCode/loa-work/src/go/libs/monitoring" "github.com/KubrickCode/loa-work/src/go/libs/schedule" @@ -27,7 +28,8 @@ func main() { log.Fatal(err) } - scraper := scraper.NewScraper(db) + client := request.NewClient() + scraper := scraper.NewScraper(client, db) converter := converter.NewConverter(db) combinedTask := func() error { diff --git a/src/go/apps/market-item-stat-scraper/scraper/scraper.go b/src/go/apps/market-item-stat-scraper/scraper/scraper.go index f1de2cc7..ee19a477 100644 --- a/src/go/apps/market-item-stat-scraper/scraper/scraper.go +++ b/src/go/apps/market-item-stat-scraper/scraper/scraper.go @@ -15,12 +15,17 @@ import ( ) type Scraper struct { + client request.APIClient db loadb.DB rateLimiter *rate.Limiter } -func NewScraper(db loadb.DB) *Scraper { - return &Scraper{db: db, rateLimiter: rate.NewLimiter(rate.Every(time.Second), 1)} +func NewScraper(client request.APIClient, db loadb.DB) *Scraper { + return &Scraper{ + client: client, + db: db, + rateLimiter: rate.NewLimiter(rate.Every(time.Second), 1), + } } func (s *Scraper) ScrapeItem() error { @@ -116,7 +121,7 @@ func (s *Scraper) getItemStatToCreate(category *models.MarketItemCategory, item return nil, fmt.Errorf("rate limiter error: %w", err) } - marketItem, err := request.GetMarketItem(&loaApi.GetMarketItemParams{ + marketItem, err := s.client.GetMarketItem(&loaApi.GetMarketItemParams{ CategoryCode: category.Code, ItemName: item.Name, ItemGrade: item.Grade, @@ -159,7 +164,7 @@ func (s *Scraper) getItemsToSave(categories []*models.MarketItemCategory) ([]*mo pageNo := 1 for { - resp, err := request.GetMarketItemList(&loaApi.GetMarketItemListParams{ + resp, err := s.client.GetMarketItemList(&loaApi.GetMarketItemListParams{ CategoryCode: category.Code, PageNo: pageNo, }) diff --git a/src/go/libs/loa-api/request/client.go b/src/go/libs/loa-api/request/client.go new file mode 100644 index 00000000..8cc5f1c0 --- /dev/null +++ b/src/go/libs/loa-api/request/client.go @@ -0,0 +1,20 @@ +package request + +import ( + "github.com/KubrickCode/loa-work/src/go/libs/loaApi" +) + +type APIClient interface { + GetAuctionItemList(params *loaApi.GetAuctionItemListParams) (*loaApi.GetAuctionItemListResponse, error) + GetCategoryList() (*loaApi.GetCategoryListResponse, error) + GetMarketItem(params *loaApi.GetMarketItemParams) (*loaApi.GetMarketItemResponse, error) + GetMarketItemList(params *loaApi.GetMarketItemListParams) (*loaApi.GetMarketItemListResponse, error) +} + +type Client struct { + api *loaApi.Client +} + +func NewClient() *Client { + return &Client{api: loaApi.NewClient()} +} diff --git a/src/go/libs/loa-api/request/get-auction-item-list.go b/src/go/libs/loa-api/request/get-auction-item-list.go index 40837a34..574e833c 100644 --- a/src/go/libs/loa-api/request/get-auction-item-list.go +++ b/src/go/libs/loa-api/request/get-auction-item-list.go @@ -7,10 +7,8 @@ import ( "github.com/pkg/errors" ) -func GetAuctionItemList(params *loaApi.GetAuctionItemListParams) (*loaApi.GetAuctionItemListResponse, error) { - client := loaApi.NewClient() - - req, err := client.NewRequest(). +func (c *Client) GetAuctionItemList(params *loaApi.GetAuctionItemListParams) (*loaApi.GetAuctionItemListResponse, error) { + req, err := c.api.NewRequest(). Method(http.MethodPost). Path("/auctions/items"). JSON(params). @@ -20,8 +18,7 @@ func GetAuctionItemList(params *loaApi.GetAuctionItemListParams) (*loaApi.GetAuc } var resp loaApi.GetAuctionItemListResponse - err = client.Do(req, &resp) - if err != nil { + if err = c.api.Do(req, &resp); err != nil { return nil, errors.Wrap(err, "GetAuctionItemList") } diff --git a/src/go/libs/loa-api/request/get-category-list.go b/src/go/libs/loa-api/request/get-category-list.go index 58f3b9e4..537ee1c9 100644 --- a/src/go/libs/loa-api/request/get-category-list.go +++ b/src/go/libs/loa-api/request/get-category-list.go @@ -7,10 +7,8 @@ import ( "github.com/pkg/errors" ) -func GetCategoryList() (*loaApi.GetCategoryListResponse, error) { - client := loaApi.NewClient() - - req, err := client.NewRequest(). +func (c *Client) GetCategoryList() (*loaApi.GetCategoryListResponse, error) { + req, err := c.api.NewRequest(). Method(http.MethodGet). Path("/markets/options"). Build() @@ -19,9 +17,8 @@ func GetCategoryList() (*loaApi.GetCategoryListResponse, error) { } var resp loaApi.GetCategoryListResponse - err = client.Do(req, &resp) - if err != nil { - return nil, errors.Wrap(err, "GetOrderList") + if err = c.api.Do(req, &resp); err != nil { + return nil, errors.Wrap(err, "GetCategoryList") } return &resp, nil diff --git a/src/go/libs/loa-api/request/get-market-item-list.go b/src/go/libs/loa-api/request/get-market-item-list.go index b35a9c6f..5b1fa8de 100644 --- a/src/go/libs/loa-api/request/get-market-item-list.go +++ b/src/go/libs/loa-api/request/get-market-item-list.go @@ -7,10 +7,8 @@ import ( "github.com/pkg/errors" ) -func GetMarketItemList(params *loaApi.GetMarketItemListParams) (*loaApi.GetMarketItemListResponse, error) { - client := loaApi.NewClient() - - req, err := client.NewRequest(). +func (c *Client) GetMarketItemList(params *loaApi.GetMarketItemListParams) (*loaApi.GetMarketItemListResponse, error) { + req, err := c.api.NewRequest(). Method(http.MethodPost). Path("/markets/items"). JSON(params). @@ -20,8 +18,7 @@ func GetMarketItemList(params *loaApi.GetMarketItemListParams) (*loaApi.GetMarke } var resp loaApi.GetMarketItemListResponse - err = client.Do(req, &resp) - if err != nil { + if err = c.api.Do(req, &resp); err != nil { return nil, errors.Wrap(err, "GetMarketItemList") } diff --git a/src/go/libs/loa-api/request/get-market-item.go b/src/go/libs/loa-api/request/get-market-item.go index c90bb6ff..435e10ed 100644 --- a/src/go/libs/loa-api/request/get-market-item.go +++ b/src/go/libs/loa-api/request/get-market-item.go @@ -10,10 +10,8 @@ import ( // 특정 거래소 아이템 정보를 조회하는 API. // 가격 정보를 포함한 응답을 위해서 복수 items 를 조회하는 API를 활용하고, 어설션 이후 첫 번째 인덱스를 반환함. -func GetMarketItem(params *loaApi.GetMarketItemParams) (*loaApi.GetMarketItemResponse, error) { - client := loaApi.NewClient() - - req, err := client.NewRequest(). +func (c *Client) GetMarketItem(params *loaApi.GetMarketItemParams) (*loaApi.GetMarketItemResponse, error) { + req, err := c.api.NewRequest(). Method(http.MethodPost). Path("/markets/items"). JSON(params). @@ -23,8 +21,7 @@ func GetMarketItem(params *loaApi.GetMarketItemParams) (*loaApi.GetMarketItemRes } var resp loaApi.GetMarketItemListResponse - err = client.Do(req, &resp) - if err != nil { + if err = c.api.Do(req, &resp); err != nil { return nil, errors.Wrap(err, "GetMarketItem") }