Go를 Java와 비교해보자

탄이·4일 전

Go학습

목록 보기
1/1
post-thumbnail

Go를 처음 익히게 됐다.
당장 Go 관련 책을 사거나 영상을 보거나, 다른 사람의 글을 읽어도 되겠지만 우선은 가장 익숙한 Java랑 비교해서 아주 러프하게라도 흐름은 따라가보도록 해보기 위해 정리했다.
공부한다고 작성한 것이라 틀린 것이 있으면 학습하면서 수정해 나가려고 한다.

Go 언어를 Java 관점에서 이해하기 위한 비교 문서

기본 개념 비교

Go 개념Java 비교설명
packagepackage동일 - 코드 그룹화
funcmethod함수 정의
structclass데이터를 담는 구조체 (클래스와 유사하나 상속 없음)
interfaceinterface동일 - 메서드 시그니처 정의
go.modpom.xml / build.gradle의존성 관리 파일
importimport동일
:=변수 선언var name = value의 축약형
ctx context.ContextThreadLocal요청 컨텍스트 전달

프로젝트 구조 비교

Java SpringGo
SpringApplication.run()main.go: main()
@Configuration 클래스config.go: LoadConfig()
Application 초기화app.go: NewApp()
Tomcat 서버 시작app.go: Run()

계층 구조 비교

계층Java SpringGo eva-vsc
진입점SpringApplication.run()main.go
설정@Configurationconfig.go
라우팅@RequestMappingrouter.go
필터Filter/Interceptormiddleware/
컨트롤러@Controllerhandler/
DTO@RequestBody 클래스message/
모델Entity/VOmodel/
서비스@Servicepkg/ 내 클라이언트들

에러 처리 비교

Java (예외 기반)

try {
    result = service.call();
} catch (Exception e) {
    // 에러 처리
}

Go (반환값 기반)

result, err := service.Call()
if err != nil {
    // 에러 처리
    return err
}
// 정상 처리

인터페이스 비교

Java (명시적 implements)

public class MyService implements Service {
    // 메서드 구현
}

Go (덕 타이핑 - 암시적 구현)

// 메서드만 구현하면 자동으로 인터페이스 충족
type PubSub interface {
    Publish(subject string, data []byte) error
    Subscribe(subject string, cb MsgHandler) error
}

// natsConnWrapper는 위 메서드들을 구현했으므로
// 자동으로 PubSub 인터페이스를 충족
type natsConnWrapper struct { ... }
func (n *natsConnWrapper) Publish(...) error { ... }
func (n *natsConnWrapper) Subscribe(...) error { ... }

비동기 처리 비교

Java (Thread)

new Thread(() -> doSomething()).start();

Go (Goroutine)

go doSomething()  // 새 고루틴에서 비동기 실행

고루틴 간 통신 (Channel)

Go의 채널은 Java의 BlockingQueue와 유사하다.

ch := make(chan Message)

// 송신
go func() {
    ch <- message
}()

// 수신
msg := <-ch

미들웨어 vs Filter

Java Servlet Filter

public void doFilter(req, res, chain) {
    // 전처리
    chain.doFilter(req, res);
    // 후처리
}

Go Middleware

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 전처리
        next.ServeHTTP(w, r)
        // 후처리
    })
}

컨트롤러 비교

Java Spring Controller

@RestController
@RequestMapping("/api")
public class ApiController {

    @Autowired
    private Service service;

    @PostMapping("/events")
    public ResponseEntity<Response> events(@RequestBody EventDTO dto) {
        // 비즈니스 로직
        return ResponseEntity.ok(result);
    }
}

Go Handler

type Handler struct {
    service Service
}

func (h *Handler) Events(w http.ResponseWriter, r *http.Request) {
    var dto EventDTO
    json.NewDecoder(r.Body).Decode(&dto)

    // 비즈니스 로직

    json.NewEncoder(w).Encode(result)
}

포인터 리시버

Go에서 func (a *App) Run()*포인터(Pointer)를 의미

Java와 비교

// Java - 클래스 안에 메서드 정의
public class App {
    public void run() {
        this.doSomething();  // this로 자기 자신 참조
    }
}

// Go 메소드 구조

func (m *AuthMiddleware) Handler(next http.Handler) http.Handler
//    └──────┬───────┘  └──┬──┘  └───────┬───────┘ └─────┬─────┘
//        리시버          메소드명        파라미터           반환타입


// Go - 구조체 밖에서 메서드 정의
type App struct {
    config Config
}

func (a *App) Run() {
    a.doSomething()  // a가 Java의 this 역할
}

* 유무의 차이

문법이름의미
func (a App)값 리시버복사본 전달 (원본 수정 불가)
func (a *App)포인터 리시버원본 참조 전달 (원본 수정 가능)

예시

type Counter struct {
    count int
}

// 값 리시버 - 원본 변경 안 됨
func (c Counter) IncrementWrong() {
    c.count++  // 복사본만 증가, 원본은 그대로
}

// 포인터 리시버 - 원본 변경됨
func (c *Counter) Increment() {
    c.count++  // 원본이 증가
}

Java로 비유하면

// 값 리시버 = 이런 느낌 (실제론 불가능)
void increment(Counter copy) {  // 복사본 전달
    copy.count++;  // 원본 영향 없음
}

// 포인터 리시버 = 일반적인 Java 메서드
void increment() {
    this.count++;  // 원본 수정
}

결론

Go에서 *가 붙은 포인터 리시버를 쓰는 이유:
1. 원본 수정이 필요할 때 (상태 변경)
2. 구조체가 클 때 (복사 비용 절약)

Java에서는 객체가 항상 참조로 전달되므로 이런 구분이 없지만, Go는 명시적으로 지정해야 한다. 대부분의 경우 *를 붙인 포인터 리시버를 사용


x := 10
p := &x   // & : x의 주소를 얻어 p에 저장 (p는 *int)
*p = 20   // * : p가 가리키는 실제 값을 수정

// & 는 주소를 만들기
// * 는 포인터를 선언하거나, 포인터를 따라가 실제 값을 읽고, 쓰기
// 포인터는 필요한 시점에 한 번 따라가서 값 읽기/쓰기를 진행함


// **결과값 + 에러를 같이 반환**하는 이 패턴이 Go 코드 어디서나 보임
// Java의 try-catch 대신 이걸 쓰는 것

func findUser(id int) (User, error) {
    user, err := db.Query(id)
    if err != nil {
        return User{}, err  // 빈 User와 에러 반환
    }
    return user, nil  // 유저와 nil(에러 없음) 반환
}

// 사용할 때
user, err := findUser(123)
if err != nil {
    // 에러 처리
}

타입

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // alias for uint8

rune // alias for int32
     // represents a Unicode code point

float32 float64

complex64 complex128

Mux

  • Spring DispatcherServlet과 비슷한 역할
  • @RequestMapping, @GetMapping 같은게 Go에서는 Mux가 하는 일
  • URL 보고 알맞은 핸들러(컨트롤러)로 연결해주는 것

Bufio (잘 안쓰겠지만)

Java의 BufferedReader와 개념적으로 유사 — I/O 횟수를 줄이기 위한 버퍼링이 핵심

공통점

Go bufioJava BufferedReader
목적버퍼를 두어 I/O 횟수 감소동일
래핑 방식기존 Reader를 감쌈기존 Reader를 감쌈
줄 읽기ReadString('\n')readLine()

비교

// Java
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line = reader.readLine();
// Go
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')  // '\n'까지 읽음

주요 차이점

  • Go의 ReadString('\n')구분자 문자('\n')를 결과에 포함시킴
  • Java의 readLine()은 구분자를 제외하고 반환
  • 그래서 Go에서는 읽은 후 strings.TrimSpace(line)으로 개행 문자를 직접 제거해야 함
profile
백엔드 개발자의 로그

0개의 댓글