controller를 추가해주고 실행시킨 뒤 해당 url로 request를 보내면 아래와 같이 정상적으로 log가 출력이 됩니다.
AddErrorController
package gg.yj.kim.controller;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class WebErrorController implements ErrorController {
@GetMapping("/error")
public String redirctRoot() {
return "index.html";
}
}
기본적으로 Spring Boot는 모든 오류를 합리적인 방식으로 처리하는 /error 매핑을 제공하며, 이는 서블릿 컨테이너에 "전역" 오류 페이지로 등록됩니다. 머신 클라이언트의 경우, 오류에 대한 세부 정보, HTTP 상태 및 예외 메시지가 포함된 JSON 응답을 생성합니다. 브라우저 클라이언트의 경우 동일한 데이터를 HTML 형식으로 렌더링하는 "화이트라벨" 오류 보기가 있습니다. 기본 동작을 완전히 바꾸려면 ErrorController를 구현하고 해당 유형의 빈 정의를 등록하는 방식이 있는데 해당 방식을 사용였습니다.
Generate된 spring boot에서 에러가 났다. 원인은 spring boot 3 버전 부터는 Java 17을 사용해야 된다고 한다. 컴퓨터에는 Java 11버전이 깔려있기 때문에 Gradle에서 Framework 버전을 2버전으로 낮춰주니 오류가 해결되었다. 관련 자료
돌려보면 아래와 같은 에러가 떴다.
> Process 'command '/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1
application.yml에 추가해준다. (application.properties를 쓰면 application.properties로 쓰면 됌)
server:
port: 8000
riot:
api:
key: api key 값
API key 값은 시스템 상의 안전을 위해서 발급받을 때 다른 사람에게 공유되지 않도록 권유한다. 그렇기때문에 application.yml을 .gitignore에 추가하여 git에 api key 값이 올라가지 않도록 설정해주어야 합니다.
root directory에 .gitignore 파일 추가 -> application.yml 추가
Frontend를 Thymeleaf로 하려했다가 Vue.js로 만들어보려 하는데, 링크를 참고했습니다.
Vue.js를 사용하려고 여러 블로그를 보니 node.js를 먼저 설치한 뒤 설치합니다. node.js를 먼저 설치하는 이유는 아래와 같이 3가지가 있었습니다.
패키지 관리
node.js는 JS의 기본 패키지 관리자인 npm과 함께 제공됩니다. npm을 사용하면 프로젝트에 필요한 Vue.js 및 기타 종속성을 설치할 수 있습니다. 종속 요소와 해당 버전을 관리하고 필요한 패키지를 자동으로 확인합니다.
빌드 도구
Vue.js 프로젝트는 일반적으로 web pack과 같은 빌드 도구를 사용하며, 여기에는 node.js가 필요합니다. 이러한 도구는 Vue 컴포넌트 컴파일, JavaScript 파일 번들링, 에셋 최적화 등과 같은 작업에 도움이 됩니다. 빌드 도구는 node.js를 사용하여 다양한 빌드 스크립트 및 구성을 실행합니다.
서버 측 렌더링
Vue.js는 서버 측 렌더링 기능도 제공하므로 서버에서 Vue.js 컴포넌트를 미리 렌더링할 수 있습니다. 이는 초기 페이지 로딩 성능과 SEO를 개성하는 데 유용합니다. Vue.js의 서버 측 렌더링은 서버에서 렌더링 프로세스를 실행하기 위해 node.js에 의존합니다.
module.exports = {
// npm run build 타겟 디렉토리
outputDir: '../src/main/resources/static',
// npm run serve 개발 진행시에 포트가 다르기때문에 프록시 설정 (port 번호는 spring boot port 번호로)
devServer: {
proxy: 'http://localhost:8000'
}
};
터미널 디렉토리를 frontend로 변경("cd frontend")하고 npm run build 명령어를 실행하면 spring boot resources/static/js 폴더와 파일이 추가된 것을 확인할 수 있다.
이후 spring boot를 실행하면 Vue.js 페이지가 나온다. (잘 배포 되었다는 증거)
Root route를 추가하고 index.html로 반환해주니 Thymeleaf와 관련된 에러가 발생했고, gradle에 Thymeleaf와 관련한 dependency를 없애주니 잘 동작했습니다.
받아온 JSON 값 중 id 값을 복사하여 LEAGUE-V4의 by-summoner에 VALUE 값으로 넣은 뒤 보내면 tier, 닉네임, point, win 수, Lose 수 등의 정보를 얻어올 수 있습니다.
puuid를 이용하여 이전 게임들의 정보를 가져올 수 있습니다. response는 matchid들 입니다.
matchid는 아래 endpoint를 통하여 게임 정보를 가져올 수 있습니다. 한 게임에 대한 방대한 데이터를 반환해주기 때문에 직접 확인해보는게 좋을 것 같음. (게임 당 10명의 닉네임, 챔피언 등등 op.gg에서 검색되는 정보들을 가져오는 Endpoint라고 생각하면 됌)
1 3 10 50 까지는 오름차순이기 때문에 pop 과정이 없지만, 20을 넣는 순간 오름차순이 깨지기 때문에 pop과정이 발생합니다.
1 3 10 50 20이 되고 모든 데이터를 넣으면 3 10502016 194930 으로 3 10 16 19 30만 남아 있는 상태가 됩니다.
사용 이유
이렇게 스택의 원소를 정렬하면, 특정 index 보다 왼쪽에 있는 값 중에서 처음으로 나오는 특정 index 미만의 수의 값을 바로 알 수 있습니다.
관련한 문제
문제를 풀어보면 모든 위치에서 오른쪽으로 볼 수 있는 정원의 개수를 구하는 문제입니다. 문제의 설명대로 구현한다면 최악의 경우 O(n^2)의 시간복잡도로 풀어야 정확하게 풀 수 있습니다. 하지만, monotone stack을 사용한다면 O(n)의 시간복잡도로 풀어낼 수 있습니다. 고민해보며 풀어보고 생각이 나지 않는다면 아래의 풀이 방법을 이해해보세요.
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws Exception {
InputReader in = new InputReader(System.in);
int n = in.nextInt();
Stack<Integer> s = new Stack<>();
long answer = 0;
for(int i = 0; i < n; i++) {
int building = in.nextInt();
while(!s.isEmpty()) {
if(s.peek() > building) break;
s.pop();
}
answer += s.size();
s.push(building);
}
System.out.println(answer);
}
}
Spring Framework에서 Spring Bean을 사용하여 객체지향 설계원칙을 지키는 방법에 대해서 공부해본 경험이 있었기때문에 궁금증이 생겼습니다. go에서는 Spring Bean과 같은 IoC(제어의 역전)을 지원해주는 도구는 없는가에 대한 궁금증이.
본론
go에서의 의존성 도구는 아래와 같은 도구들이 있습니다.
wire
fx(built on top of dig)
wire는 google에서 만들었으며 code generator 방식으로 build-time에 코드를 만들어내는 도구입니다. fx는 uber에서 만들었으며 reflect를 이용해서 runtime에 의존성을 주입하는 도구입니다. 각각의 도구들을 사용해봅시다.
wire
wire는 providers와 injectors라는 핵심 개념이 있다.
아래의 명령어를 실행합니다.
go get github.com/google/wire/cmd/wire
Defining Providers
Wire의 기본 메커니즘은 값을 생성할 수 있는 함수인 provide입니다.
package foobarbaz
type Foo struct {
X int
}
// ProvideFoo returns a Foo.
func ProvideFoo() Foo {
return Foo{X: 42}
}
provider 함수는 일반 함수와 마찬가지로 다른 패키지에서 사용하기 위해 내보내야 합니다. provider는 매개변수로 종속성을 지정할 수 있습니다.
package foobarbaz
// ...
type Bar struct {
X int
}
// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}
providers는 error를 return 할 수도 있습니다.
package foobarbaz
import (
"context"
"errors"
)
// ...
type Baz struct {
X int
}
// ProvideBaz returns a value if Bar is not zero.
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
if bar.X == 0 {
return Baz{}, errors.New("cannot provide baz when bar is zero")
}
return Baz{X: bar.X}, nil
}
공급자는 공급자 집합으로 그룹화할 수 있습니다. 이는 여러 공급자가 자주 함께 사용되는 경우에 유용합니다. 이러한 공급자들을 SuperSet이라는 새 세트에 추가하려면 wire.NewSet function을 사용하면됩니다.
애플리케이션은 종속성 순서대로 provider를 호출하는 함수인 injector를 사용하여 wire로 연결합니다. Wire를 사용하면 injector의 서명을 작성하면 Wire가 함수의 본문을 생성합니다.
인젝터는 wire.Build에 대한 호출을 본문으로 하는 함수 선언을 작성하여 선언됩니다. 반환 값은 올바른 유형인 한 중요하지 않습니다. 생성된 코드에서 값 자체는 무시됩니다. 위의 provider가 go/providers/foobarbaz라는 패키지에 정의되어 있다고 가정해 보겠습니다. 다음은 Baz를 얻기 위한 injector를 선언합니다:
// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package main
import (
"context"
"github.com/google/wire"
"go/providers/foobarbaz"
)
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.SuperSet)
return foobarbaz.Baz{}, nil
}
이후 위 코드가 담긴 디렉토리 안에서 아래의 명령어를 실행합니다.
wire
저는 해당 과정에서 zsh: command not found: wire 에러가 발생했고, export PATH=$PATH:$GOPATH/bin 명령어로 path를 수정해주니 정상적으로 돌아갔습니다. 해결과정에서 참고한 링크입니다.
wire 명령어를 치는순간 아래와 같은 코드로 wire_gen.go라는 파일이 자동으로 생성됩니다.
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package main
import (
"context"
"go/providers/foobarbaz"
)
// Injectors from wire.go:
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
foo := foobarbaz.ProvideFoo()
bar := foobarbaz.ProvideBar(foo)
baz, err := foobarbaz.ProvideBaz(ctx, bar)
if err != nil {
return foobarbaz.Baz{}, err
}
return baz, nil
}
순서대로 잘 생성된 모습을 볼 수 있고 wire를 통해 의존성을 주입하고 최종적으로 의존성을 주입받은 객체를 반환받습니다. 더 자세한 기능들은링크에서 확인할 수 있습니다.
총 코드
foo, bar, baz 코드
package foobarbaz
import (
"context"
"errors"
)
type Foo struct {
X int
}
type Bar struct {
X int
}
type Baz struct {
X int
}
// ProvideFoo returns a Foo.
func ProvideFoo() Foo {
return Foo{X: 42}
}
// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}
// ProvideBaz returns a value if Bar is not zero.
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
if bar.X == 0 {
return Baz{}, errors.New("cannot provide baz when bar is zero")
}
return Baz{X: bar.X}, nil
}
wire build 코드
// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package wire
import (
"context"
"github.com/google/wire"
"go/providers/foobarbaz"
)
func InitializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.ProvideFoo, foobarbaz.ProvideBar, foobarbaz.ProvideBaz)
return foobarbaz.Baz{}, nil
}
생성된 wire_gen.go 코드
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package wire
import (
"context"
"go/providers/foobarbaz"
)
// Injectors from wire.go:
func InitializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
foo := foobarbaz.ProvideFoo()
bar := foobarbaz.ProvideBar(foo)
baz, err := foobarbaz.ProvideBaz(ctx, bar)
if err != nil {
return foobarbaz.Baz{}, err
}
return baz, nil
}
기존에 생성코드로 만든 baz와 wire로 생성된 baz 모두 같은 값을 출력하는 것을 확인할 수 있다. Foo의 X값을 변경해도 변경된 값으로 출력된다. 의존성과 파라미터, return 형태에 변경만 없다면 내부 로직을 바꾸어도 정상적으로 돌아가는 것을 확인할 수 있다.
하지만 의존성/파라미터/return 형태에 변경이 있다면 코드를 변경해준 뒤 wire 명령어를 다시 입력해야 한다.
fx
fx는 서버 애플리케이션의 구성 요소를 생성하고 연결하는 데 도움을 주면서 DI를 지원하는 프레임워크입니다.
사용하면서 느낀건데 go에서 사용되는 다른 web framework들도 DI를 제공합니다. (예를들어, Fiber / Gin / Echo 등이 있다.)
사용방법
아래의 명령어를 실행합니다.
go get go.uber.org/fx@v1
main.go
package main
import "go.uber.org/fx"
func main() {
fx.New().Run()
}
run
go run .
아래와 같은 출력을 확인할 수 있습니다.
[Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] RUNNING
인자 없이 fx.New를 호출하여 빈 Fx 애플리케이션을 빌드했습니다. 애플리케이션은 일반적으로 fx.New에 인수를 전달해 컴포넌트를 설정하고 App.Run 메서드로 이 애플리케이션을 실행합니다. ctrl+c를 누르면 종료가 됩니다.
Fx는 주로 장기 실행 서버 애플리케이션을 위한 것으로 종료할 때가 되면 배포 시스템으로부터 신호를 받습니다.
Add an HTTP server
HTTP 서버를 구축하는 함수를 작성
// NewHTTPServer builds an HTTP server that will begin serving requests
// when the Fx application starts.
func NewHTTPServer(lc fx.Lifecycle) *http.Server {
srv := &http.Server{Addr: ":8080"}
return srv
}
하지만 이것만으로는 충분하지 않습니다. Fx에게 HTTP 서버를 시작하는 방법을 알려줘야 합니다. 이것이 바로 추가 fx.Lifecycle 인수의 목적입니다. fx.Lifecycle 객체를 사용하여 애플리케이션에 수명 주기 후크를 추가합니다. 이렇게 하면 Fx가 HTTP 서버를 시작하고 중지하는 방법을 알려줍니다.
돌려보면 정상적으로 돌아가는 것 같지만 위에 적어놓은 fmt.Println("Starting HTTP server at", srv.Addr)부분이 출력되지 않는 것을 확인할 수 있습니다. 서버가 시작되지 않았다는 것인데요. 실제로 curl로 요청을 보내보면 아래와 같은 에러로그가 출력됩니다.
curl: (7) Failed to connect to localhost port 8080 after 14 ms: Couldn't connect to server
이제 돌려보면 정상적으로 돌아갑니다. curl로 요청을 보내면 404가 뜨지만, 적절한 응답이 없어서 뜨는 에러입니다.
404 page not found
그렇다면 적절한 응답을 보내기 위해 Handler를 추가해봅시다.
// EchoHandler is an http.Handler that copies its request body
// back to the response.
type EchoHandler struct{}
// NewEchoHandler builds a new EchoHandler.
func NewEchoHandler() *EchoHandler {
return &EchoHandler{}
}
// ServeHTTP handles an HTTP request to the /echo endpoint.
func (*EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if _, err := io.Copy(w, r.Body); err != nil {
fmt.Fprintln(os.Stderr, "Failed to handle request:", err)
}
}
다음으로 *http.ServeMux를 빌드하는 함수를 작성합니다. http.ServeMux는 서버가 수신한 요청을 다른 핸들러로 라우팅합니다. 우선, 이 함수는 /echo로 전송된 요청을 *EchoHandler로 라우팅하므로 생성자는 *EchoHandler를 인수로 받아들여야 합니다.
// NewServeMux builds a ServeMux that will route requests
// to the given EchoHandler.
func NewServeMux(echo *EchoHandler) *http.ServeMux {
mux := http.NewServeMux()
mux.Handle("/echo", echo)
return mux
}
$ curl -X POST -d 'hello kyj' http://localhost:8080/echo
hello kyj
현재는 프로그램을 돌리면 아래와 같은 출력이 발생합니다. 이를 로거로 바꿔주겠습니다.
[Fx] PROVIDE *http.Server <= main.NewHTTPServer()
[Fx] PROVIDE *http.ServeMux <= main.NewServeMux()
[Fx] PROVIDE *main.EchoHandler <= main.NewEchoHandler()
[Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] INVOKE main.main.func1()
[Fx] HOOK OnStart main.NewHTTPServer.func1() executing (caller: main.NewHTTPServer)
Starting HTTP server at :8080
[Fx] HOOK OnStart main.NewHTTPServer.func1() called by main.NewHTTPServer ran successfully in 320.824µs
[Fx] RUNNING
^C[Fx] INTERRUPT
[Fx] HOOK OnStop main.NewHTTPServer.func2() executing (caller: main.NewHTTPServer)
[Fx] HOOK OnStop main.NewHTTPServer.func2() called by main.NewHTTPServer ran successfully in 241.698µs
여기는 Zap을 사용했습니다. 사용하기 위해서는 아래 명령어를 실행해주고 "go.uber.org/zap"을 import 해야합니다.
위 명령어로 컴파일을 하면 grpc.pb.go 파일이 생성된다. 후에 이 파일의 인터페이스로 코드를 작성하면 된다.
생성된 코드를 확인해보면 다음과 같다.
// 테스트
service Test {
rpc Show(TestRequest) returns (TestResponse);
}
.proto 파일에서 작성한 service가 client와 server 각각 interface로 생성되어있다.
// TestClient is the client API for Test service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type TestClient interface {
Show(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (*TestResponse, error)
}
// TestServer is the server API for Test service.
// All implementations must embed UnimplementedTestServer
// for forward compatibility
type TestServer interface {
Show(context.Context, *TestRequest) (*TestResponse, error)
mustEmbedUnimplementedTestServer()
}
그리고 아래와 같이 rpc로 생성한 기능이 생성된 것을 볼 수 있다. gRPC Client와 Server로 함수처럼 이용이 가능하며 내부 로직을 몰라도 사용이 가능하다. 매우 Simple하다.
// Client
type testClient struct {
cc grpc.ClientConnInterface
}
func NewTestClient(cc grpc.ClientConnInterface) TestClient {
return &testClient{cc}
}
func (c *testClient) Show(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (*TestResponse, error) {
out := new(TestResponse)
err := c.cc.Invoke(ctx, "/grpc.Test/Show", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server
// UnimplementedTestServer must be embedded to have forward compatible implementations.
type UnimplementedTestServer struct {
}
func (UnimplementedTestServer) Show(context.Context, *TestRequest) (*TestResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Show not implemented")
}
func (UnimplementedTestServer) mustEmbedUnimplementedTestServer() {}
그렇다면, gRPC Client와 Server의 코드를 작성해보자.
파일의 구조는 위와 같다.
server.go
package main
import (
"context"
"log"
"net"
pb "go/grpc"
"google.golang.org/grpc"
)
type server struct {
pb.TestServer
}
func (s *server) Show(ctx context.Context, in *pb.TestRequest) (*pb.TestResponse, error) {
log.Printf("Received: %v", in.GetRequestString())
return &pb.TestResponse{ResponseString: "Hello " + in.GetRequestString()}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterTestServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
server 측에선 특정 port로 연결을 대기해놓는다. 참고로 REST API의 경우 요청마다 연결을 성립하지만 gRPC의 경우 한번 연결을 해놓으면 해당 연결을 재사용하며 API를 주고 받는다.
그렇다면? gRPC는 하나의 server당 하나의 client만 연결할 수 있는가?? (후에 정리)
client.go
package main
import (
"context"
"log"
"time"
pb "go/grpc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewTestClient(conn)
// Contact the server and print out its response.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.Show(ctx, &pb.TestRequest{RequestString: "KYJ"})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Test: %s", r.GetResponseString())
}
Client에서는 주소:port로 connection을 만들고 connection으로 Client를 생성합니다. 그리고 해당 Client의 interface에 정의된 함수로 요청을 Server측으로 보낼 수 있습니다.