gRPC란?

google에서 개발한 RPC(Remote Procedure Call)이다. gRPC는 모든 환경에서 실행할 수 있는 오픈 소스 RPC 프레임워크이다. HTTP/2기반으로 다양한 언어에서 사용이 가능하며 스텁, 프로토콜버퍼 등의 특징이 있다.

 

RPC란?

프로세스 간 통신(IPC)의 형태로 떨어져 있거나 분산되어 있는 컴퓨터간의 통신을 위한 기술이다. 중요한 것은 한 프로그램이 네트워크의 세부 정보를 이해하지 않고도 네트워크 안의 다른 컴퓨터에 있는 프로그램에 서비스를 요청할 수 있는 프로토콜이라고 보면 된다. RPC는 client-server 모델을 사용한다. client에서 서비스를 요청(function call)하면, server에서 서비스를 제공하는 형태이다.

Implementation of RPC mechanism

클라이언트 -> stub -> RPC Runtime -> RPC Runtime -> Server stub -> Server 과정의 매커니즘이다.

 

Stub이란?

RPC의 핵심 개념으로 매개변수(parameter) 객체를 메시지(Message)로 변환(Marchalling)/역변환(Unmarshalling)하는 레이어이며 클라이언트의 스텁과 서버의 스텁으로 나뉘어 클라이언트와 서버 간 통신에 필요한 코드를 생성하는 역할을 한다.

기본적으로 gRPC는 Protobuf를 사용하여 서비스를 정의한다. stub을 사용함으로써, 클라이언트는 서비스의 메소드를 직접 호출하지 않고, 스텁을 통해 메소드로의 요청을 전달하고, 스텁이 반환한 프로토콜 버퍼를 클라이언트 코드로 변환한다. 이와 같은 프로세스는 클라이언트와 서버 모두에서 간단하고 일관된 인터페이스를 제공한다.

또한, stub은 작동하는 데 필요한 모든 코드를 자동으로 생성하므로 쉬운 업데이트와 유지관리가 가능하며 stub을 통해 클라이언트와 서버가 요청과 응답을 하기때문에 언어에 자유도가 있다.
  • 클라이언트 스텁
    • 함수 호출에 사용된 파라미터의 변환 및 함수 실행 후 서버에서 전달된 결과의 변환
  • 서버 스텁
    • 클라이언트가 전달한 매개 변수의 역변환 및 함수 실행 결과 변환

gRPC

위의 그림처럼 스텁을 이용해서 서버와 클라이언트간의 언어가 달라도 요청과 응답을 구현할 수 있다. .proto파일을 protoc를 사용하여 생성된 코드는 아래와 같다. (해당 코드가 stub이라고 일컫는 것 같다. --- 추정)

// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc             v3.21.12
// source: grpc/grpc.proto

package hello

import (
	context "context"
	grpc "google.golang.org/grpc"
	codes "google.golang.org/grpc/codes"
	status "google.golang.org/grpc/status"
)

// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7

// 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)
}

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
}

// 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()
}

// 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() {}

// UnsafeTestServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to TestServer will
// result in compilation errors.
type UnsafeTestServer interface {
	mustEmbedUnimplementedTestServer()
}

func RegisterTestServer(s grpc.ServiceRegistrar, srv TestServer) {
	s.RegisterService(&Test_ServiceDesc, srv)
}

func _Test_Show_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
	in := new(TestRequest)
	if err := dec(in); err != nil {
		return nil, err
	}
	if interceptor == nil {
		return srv.(TestServer).Show(ctx, in)
	}
	info := &grpc.UnaryServerInfo{
		Server:     srv,
		FullMethod: "/grpc.Test/Show",
	}
	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
		return srv.(TestServer).Show(ctx, req.(*TestRequest))
	}
	return interceptor(ctx, in, info, handler)
}

// Test_ServiceDesc is the grpc.ServiceDesc for Test service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Test_ServiceDesc = grpc.ServiceDesc{
	ServiceName: "grpc.Test",
	HandlerType: (*TestServer)(nil),
	Methods: []grpc.MethodDesc{
		{
			MethodName: "Show",
			Handler:    _Test_Show_Handler,
		},
	},
	Streams:  []grpc.StreamDesc{},
	Metadata: "grpc/grpc.proto",
}

위처럼 Client와 Server 별로 interface가 생성되고 개발자는 해당 interface를 통해 gRPC를 메소드처럼 사용하기만 하면 된다.

 

 

프로토콜버퍼(protocol buffer)

gRPC는 IDL(Interface Definition Language : 정보를 저장하는 규칙)로 프로토콜버퍼(protocol buffer)를 사용한다.
프로토콜 버퍼는 구조화된 데이터를 직렬화하기 위한 구글의 언어 중립적, 플랫폼 중립적, 확장 가능한 메커니즘을 말합니다. 보통 XML이나 JSON을 생각할 수 있지만 더 작고 빠르고 간단하다.

아래는 .proto 파일을 protoc를 통해 생성한 IDL된 코드이다. 

	RequestString string `protobuf:"bytes,1,opt,name=requestString,proto3" json:"requestString,omitempty"`

protobuf로 byte로 데이터를 직렬화 하는 모습을 볼 수 있다. 자세한 직렬화하는 과정은 이후에 정리해보도록 하자.

 

IDL (Interface Definition Language)

대표적인 IDL로는 JSON, XML, Proto buffer(proto)가 있다. 

IDL 대표 3개 비교

Proto가 가장 빠르며 비용도 적은 모습을 볼 수 있습니다.

 

 

gRPC의 구조

gRPC는 애플리케이션, 프레임워크, 전송계층으로 나눠져 구성되어있다. 지금 보면 HTTP2 위에 올라가있는 것을 알 수 있다.

gRPC 구조

 

gRPC의 채널

gRPC는 여러 서브채널을 열어서 통신한다. 이 채널을 재사용함으로써 통신비용을 줄일 수 있다. gRPC는 HTTP 2.0을 사용하는데 HTTP 1.1의 단점을 보완한 기술이다.

gRPC Channel

코드로 보자.

	// Server
	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)
	}


	// Client 
	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)

위처럼 Server에서 특정 port로 Server를 해주고, Client에서 연결해주면 Channel이 생성된다. 해당 Channel을 통하여 통신을 주고받는다.

 

 

gRPC와 RESTAPI 비교

gRPC는 다음과 같이 프로토콜버퍼를 중심으로 통신하지만 REST API의 경우 HTT로 통신한다. gRPC-WEB은 gRPC로만 통신하는 웹서비스를 뜻한다.

 

워크플로우

gRPC를 이용한 workflow는 아래와 같다. protobufs를 정의한 다음 protoc 컴파일러를 통해 여러가지 언어를 기반으로 서버와 클라이언트가 형성된다.

gRPC workflow

 

장점

gRPC는 대부분의 아키텍쳐에 사용할 수 있지만 MSA에 가장 적합한 기술이다. 많은 서비스 간의 API 호출로 인한 성능 저하를 개선하며 보안, API 게이트웨이, 로깅등을 개발하기 쉽다.

위의 그림처럼 MSA 환경에서 gRPC는 지연시간 감소에서 강력한 강점을 가진다고 한다. (?? 아직 모르겠음)

 

  • 높은 생산성, 프로토콜 버퍼의 IDL만 정의하면 서비스와 메세지에 대한 소스코드가 자동으로 생성되고 데이터를 주고 받을 수 있다.
  • gRPC는 HTTP/2 기반으로 통신하며 이 때문에 양방향 스트리밍이 가능하다.
  • gRPC는 HTTP/2 레이어 위에서 프로토콜 버퍼를 사용해 "직렬화된 바이트 스트림"으로 통신하여 JSON 기반의 통신보다 더 가볍고 통신 속도가 빠르다.
  • 런타임 환경과 개발환경을 구축하기가 굉장히 쉽다.
  • 다양한 언어를 기반으로 만들 수 있다.

단점

  • 브라우저에서 gRPC 서비스 직접 호출이 불가능하다. 
    • 하지만, 프록시 서버를 통해서 쉽게 해결할 수 있음

 

 

 

 

 

 

참고자료

'Go' 카테고리의 다른 글

Import/Export  (0) 2023.12.24
package와 main  (0) 2023.12.24
Go에서 의존성 주입 (wire, fx 사용방법)  (0) 2023.05.14
go로 gRPC Unary 찍먹  (0) 2023.05.01
Golang 설정 및 실행  (0) 2023.04.30

Tool

  • vscode

 

Golang install

homebrew를 이용해서 설치 가능 (최신 버전으로 설치)
!이전 버전으로 설치하고 싶은 경우 go@1.16과 같은 방법으로 설치가 가능
brew install go

 

go version 확인

go version

 

go env

잘 못된 방식으로 설치를 하다가 GOPATH 경로가 이상하게 설정되어 아래와 같이 수정해줌
/usr/local/go path는 golang이 설치된 경로를 확인한 다음 수정
export GOPATH="/usr/local/go"

 

go 파일 실행

vscode로 .go 파일을 만들고 아래와 같이 코드 작성
package main

import "fmt"

func main() {
    fmt.Println("hello world!")
}

cmd 에서 아래 코드 실행

go run 파일명.go

출력

hello world!

 

 

'Go' 카테고리의 다른 글

Import/Export  (0) 2023.12.24
package와 main  (0) 2023.12.24
Go에서 의존성 주입 (wire, fx 사용방법)  (0) 2023.05.14
go로 gRPC Unary 찍먹  (0) 2023.05.01
gRPC란?  (0) 2023.05.01

Spring에 대한 공부를 하며, Spring에서 제공하는 DI 덕분에 객체지향 설계 원칙인 SOLID 중 지켜지지 않던 O(OCP), D(DIP)를 지키며 개발할 수 있다는 것에 공감할 수 있었다. 문득, DI에 대해 공부해보며 궁금증이 생긴 부분이 있었고 이를 Spring에서 어떻게 해결해주는지에 대하여 알아본 결과를 정리해보았다.

 

궁금증....만약 인터페이스 1개에 구현체가 2개있는 경우는 의존성 주입이 어떻게 될까??


실제 코드를 통해 알아 보았다. 아래와 같은 Car라는 Interface가 있다.

public interface Car {
    int go();
}

해당 인터페이스를 구현하는 구현체 k5, k7이 아래와 같이 작성되있다고 가정해보자. 참고로 @Compenent의 경우 해당 클래스를 Spring Bean으로 등록한다는 것을 말한다.

@Component
public class k5 implements Car{

    @Override
    public int go() {
        return 5;
    }
}
@Component
public class k7 implements Car{

    @Override
    public int go() {
        return 7;
    }
}

그리고 테스트를 진행해 보았다.

	@Autowired
	private Car car;

참고로, @Autowired의 경우 Car Interface에 맞는 Bean으로 등록 된 객체를 가져와 Spring에서 자동으로 DI를 해달라고 명시해주는 어노테이션이다. 하지만, car 부분에 빨간줄이 생기며 제대로 DI가 안된다는 것을 알 수 있었다.

원인의 에러는 다음과 같았다.

 

Could not autowire. There is more than one bean of 'Car' type.
Beans: k5   (k5.java) 
       k7   (k7.java) 

직역해보면 autowire할 수 없다. 'Car' type의 bean이 한개보다 많다. 라고 한다.

그렇다. 구현체가 두개 이상 있는 경우에는 역시나 DI를 제대로 하지 못한다.

 

 

그렇다면, 해결 방법으로는 어떤 것이 있을까??


  1. 변수 네이밍을 Bean 이름으로 한다.
  2. @qualifier("Bean 객체 이름")을 사용한다.
  3. @primary를 사용한다.
  4. 사용하는 구현체 1개만 @component를 유지하고 나머지 구현체의 @component를 지워 Bean에 구현체 한개만 등록되도록 한다.

4 가지의 방법을 직접 코드로 구현해보며, 각각의 장단점에 대하여 느껴보자.

 

 

1번째 방법을 통해 해결해본 코드이다.

	@Autowired
	private Car k5;

결과적으로 제대로 DI를 통해 객체를 주입받지만, SOLID의 원칙인 OCP와 DIP가 위배된 코드를 확인할 수 있다.

 

 

2번째 방법을 통해 해결해본 코드이다.

	@Qualifier("k5")
	@Autowired
	private Car car;

결과적으로 제대로 DI를 통해 객체를 주입받고, OCP도 지키며 DIP도 지킨다고 볼 수 있다. Spring에서도 @qualifier를 추가하여 문제를 해결하라고 안내하긴 한다. 하지만,, @qualifier("k5")를 사용하다가 후에 k7으로 주입하고 싶으면 @qualifier("k5")

 

 @qualifier("k7") 변경 작업이 불가피하기때문에 결과적으로 보면 OCP와 DIP를 위반한다고 볼 수 있다.

 

3번째 방법을 통해 해결해본 코드이다.

@Component
@Primary
public class k5 implements Car{

    @Override
    public int go() {
        return 5;
    }
}

해당 @primary의 경우 구현체 중 우선순위를 정해준 것이기 때문에, 같은 interface의 구현체 중 1개만 정해야 하는 단점이 있지만, 테스트 클래스에서 수정하는 것이 아닌 구현체 클래스에서 수정하는 것을 볼 수 있다. 또한, OCP와 DIP도 지켜주는 모습이다.

 

4번째 방법을 통해 해결하는 방법은 사용하는 구현체 딱 한 개 말고는 @component를 삭제하여 Bean에 등록을 안시켜주면 된다. Spring에서 Interface에 의존성 주입을 할 때 1개의 구현체만 있는경우는 문제가 생기지 않기 떄문이다.

 

결과적으로, 3번째와 4번째 방법이 좋은 해결 방법이라고 볼 수 있다.

가장 좋은 방법은 4번째 방법으로, 사용할 하나의 구현체만 Bean으로 등록하는 것이 좋은 방법인 것 같다.

하지만, 여러개의 구현체를 두고 상황에 맞춰서 각각 다른 구현체를 사용해야 한다면 2번째 방법을 통해 해결하는 것이 최선인 것 같다.

혹시나, 나중에 구현체를 두개이상 두는 경우 해당 방법으로 해결하면 좋을 것 같다.

'spring > 정리' 카테고리의 다른 글

로그  (0) 2022.06.04
혼자서 하는 개발보다 많은 사람들과 협력하여 개발하는 일이 많다. 또한, 실제로 기능을 개발하는 일보다 유지보수하는 일이 많다고 한다. 그렇기때문에, 코드는 다른 사람이 보고 이해하기 쉽도록 할 필요가 있다.

 

 

개발, 강의를 보며 더 깔끔하게 코드를 작성할 수 있는 요령을 발견한다면 이에 대하여 정리할 예정이다. 

 

 

 

Tell Don`t Ask

다른 객체에 데이터를 요청해서 변경하고 저장하라고 하지말고 기능을 실행하라! 즉,데이터를 잘 알고 있는 객체에게 기능을 수행하라고 하라. 이렇게되면, Encapsulation이 유지되어 변경에 영향을 안 받게 된다.

예를들어 보자. 넷플릭스에서 회원의 구독이 만료되어있는지, 만료되지 않았는지 확인하는 코드로 예시를 들어보겠다.

if(member.getExpiredDate().getTime() < System.currentTimeMillis)

클라이언트에서 위처럼 코드를 작성하면 member객체에 저장되어 있는 만료날짜를 가져와 현재 시간보다 적으면 만료되지 않았다는 것을 알 수 있다.

 

하지만, 이보다는 아래와 같은 방식이 역할과 책임을 잘 분리하고 유지보수하기 수월하다.

if(member.isExpired())

위처럼 설계해야 하는 이유는 두가지가 있다. 

1. 만료가 되었는지 되지 않았는지는 Member 객체에 대한 책임이고, 수행해야할 역할이다. 

2. 유지보수하기 수월하다.

 

여기서 유지보수하기가 수월하다는 이유가 와닿지 않을 수도 있다. 그렇다면, 가정해보자. 처음 설계는 밀리세컨드로 만료날짜를 확인했다. 하지만, 후에 밀리세컨드가 아닌 세컨드로 만료날짜를 확인하기로 변경되었다면 클라이언트 코드 중 어디서 만료날짜를 확인했는지부터 찾아야할 것이다. 하지만, 만료날짜를 확인하는 클라이언트 코드가 많은 경우라면? 코드를 다 찾고 수정하기 어렵다는 것을 알 수 있다.

 

하지만, 2번째와 같이 코드가 되어있다면 Member 클래스에서 isExpired() 메소드 안에 코드만 한번 수정해주면 끝난다. 이처럼 초반 설계는 중요하다..!

 

 

 

 

 

 

 

 

'JAVA' 카테고리의 다른 글

절차지향 vs 객체지향  (0) 2022.11.04
[Java 8] Metaspace  (0) 2022.10.18
[JAVA 8] ParallelSort  (0) 2022.10.15
[Java 8] CompletableFuture  (0) 2022.10.13
[Java 8] Date와 Time  (0) 2022.10.12

절차지향 vs 객체지향

절차지향                 /                 객체지향

절차지향

하나의 데이터로 모든 프로시저가 공유하고 있기때문에 데이터를 변경할 경우 많은 프로시저에 영향을 끼친다. 이때문에 데이터 변경이 어렵다.ㅜ웛
  • 장점
    • 유지보수를 신경쓰지 않아도되기 때문에, 설계부터 구현까지 객체지향에 비해 시간이 오래걸리지 않는다.
  • 단점
    • 수정이 어렵기때문에 유지보수가 필요한 대형 프로젝트에서 사용하기에 부적합하다.

 

객체지향

데이터와 코드가 객체로 캡슐화 되어있다. 데이터와 코드가 캡슐화되어있는 객체끼리 서로 상호작용한다.
  • 장점
    • 절차지향과 다르게 데이터와 코드의 변경은 외부에 영향을 미치지않는다.
    • 외부에 노출된 인터페이스만 변경되지 않는다면 프로시저를 실행하는데 필요한 만큼의 데이터만 가진다.
    • 비교적 유지보수가 수월하기때문에 대형 프로젝트에서 사용하기에 적합하다.
    • 객체끼리 서로 상호작용하기 때문에, 코드의 재사용성을 높일 수 있다.
  • 단점
    • 절차지향에 비해 속도가 느리다.
    • 설계에 시간을 사용한만큼 유지보수가 수월하기때문에, 설계단계에서 시간이 많이 소모된다.

 

 

 

'JAVA' 카테고리의 다른 글

Clean Code  (0) 2022.11.04
[Java 8] Metaspace  (0) 2022.10.18
[JAVA 8] ParallelSort  (0) 2022.10.15
[Java 8] CompletableFuture  (0) 2022.10.13
[Java 8] Date와 Time  (0) 2022.10.12

JVM의 여러 메모리 영역 중에 PermGen 메모리 영역이 없어지고 Metaspace 영역이 생겼다.

 

PermGen

  • permanent generation(eden generation, old generation와 비슷한 영역), 클래스 메타데이터를 담는 곳.
  • Heap 영역에 속함
  • 기본값으로 제한된 크기를 가지고 있음
  • -XX:PermSize=N, PermGen 초기 사이즈 설정
  • -XX:MaxPermSize=N, PermGen 최대 사이즈 설정

 

Metaspace

  • 클래스 메타데이터를 담는 곳
  • Heap 영역(Eden, Old)이 아니라, Native 메모리 영역이다.
  • 기본값으로 제한된 크기를 가지고 있지 않다. (필요한 만큼 계속 늘어난다.)
  • Java 8 부터는 PermGen 관련 java 옵션은 무시한다.
  • -XX:MetaspaceSize=N, Metaspace 초기 사이즈 설정.
  • -XX:MaxMetaspaceSize=N, Metaspace 최대 사이즈 설정.

 

참고

 

'JAVA' 카테고리의 다른 글

Clean Code  (0) 2022.11.04
절차지향 vs 객체지향  (0) 2022.11.04
[JAVA 8] ParallelSort  (0) 2022.10.15
[Java 8] CompletableFuture  (0) 2022.10.13
[Java 8] Date와 Time  (0) 2022.10.12

Arrays.parallelSort()

  • Fork/Join 프레임워크를 사용해서 배열을 병렬로 정렬하는 기능을 제공한다.

 

병렬 정렬 알고리즘

  • 배열을 둘로 계속 쪼갠다.
  • 합치면서 정렬한다.

 

이전에 parallelStream을 설명할 때 말한 적이 있다. 상황에 따라서 병렬로 하는게 빠를 수도 있고, 더 느릴 수도 있다고 말이다. 실제로 그렇게 되는지 확인해보자.

 

sort()와 parallelSort() 비교

int size = 10000000;
int[] numbers = new int[size];
Random random = new Random();
IntStream.range(0, size).forEach(i -> numbers[i] = random.nextInt());

long start = System.nanoTime();
Arrays.sort(numbers);
System.out.println("  serial sorting took " + (System.nanoTime() - start));

IntStream.range(0, size).forEach(i -> numbers[i] = random.nextInt());
start = System.nanoTime();
Arrays.parallelSort(numbers);
System.out.println("parallel sorting took " + (System.nanoTime() - start));
위의 코드에서 size만 변경해보며 시간이 얼마나 걸리는지 차이를 알아보자.

 

size가 1,000인 경우

  • serial sorting took 635,363
  • parallel sorting took 2,320,564

 

size가 10,000인 경우

  • serial sorting took 4,114,773
  • parallel sorting took 5,344,624

 

size가 100,000인 경우

  • serial sorting took 39,835,655
  • parallel sorting took 42,495,858

 

size가 1,000,000인 경우 

  • serial sorting took 967,621,466
  • parallel sorting took 333,204,237
  • 알고리즘 효율성은 같다. 시간복잡도 : O(nlogN), 공간복잡도 : O(n)

 

size가 10,000,000인 경우

  • serial sorting took 968,464,268
  • parallel sorting took 277,913,894

 

size가 100,000,000인 경우

  • serial sorting took 10,152,806,777
  • parallel sorting took 2,313,726,436
size를 10단위로 늘려가며 테스트를 해보니 값이 커질 수록 병렬처리가 빠른 것을 확인해볼 수 있습니다.

 

그렇다면, size가 작으면 병렬이 느리고 size가 크면 병렬이 빠른 이유는 무엇일까요??

 

일단, 직렬 프로그래밍과 병렬 프로그래밍이 어떻게 다른지 알아야합니다.

 

프로세스에서 여러가지 일을 수행하기 위해선 멀티쓰레드를 사용해야 합니다. 단일쓰레드는 직렬 프로그래밍, 멀티쓰레드는 병렬 프로그래밍에서 사용한다고 보면됩니다. 여튼, 병렬 프로그래밍에서 사용하는 쓰레드는 쓰레드풀이 있지만, 쓰레드를 만들어 사용한다고하면 시간이 오래걸립니다. 이 때문에 실제로 size가 작을 때 작은 size를 계산하는 동안 쓰레드를 가져오는데 시간을 소모하기때문에 더 시간이 오래 걸리는 것 입니다. 

 

실제로 size가 100일 때 시간 차이를 확인해 보았습니다. 

public class SortTest {
    public static void main(String[] args) {
        int size = 100;
        int[] numbers = new int[size];
        Random random = new Random();
        IntStream.range(0, size).forEach(i -> numbers[i] = random.nextInt());

        long start = System.nanoTime();
        Arrays.sort(numbers);
        System.out.println("  serial sorting took " + (System.nanoTime() - start));

        IntStream.range(0, size).forEach(i -> numbers[i] = random.nextInt());
        start = System.nanoTime();
        Arrays.parallelSort(numbers);
        System.out.println("parallel sorting took " + (System.nanoTime() - start));



        start = System.nanoTime();
        MyThread myThread = new MyThread();
        myThread.start();
        System.out.println("Thread took " + (System.nanoTime() - start));
    }

    public static class MyThread extends Thread{
        int num;

        public MyThread() {
            this.num = 0;
        }

        public MyThread(int num) {
            this.num = num;
        }

        @Override
        public void run() {
            System.out.println("Thread Test");
        }
    }
}

 

출력

size 100일 때 시간

serial sorting 작업은 399,160입니다. 1개의 쓰레드를 생선하는데 필요한 시간은 1,075,145로 100개를 직렬로 정렬하는 것보다 더 오랜 시간을 소모하는 것을 확인할 수 있습니다. 그렇기 때문에 size에 따라서 직렬로 사용할지 병렬로 사용할지 잘 판단하여야 합니다.

 

'JAVA' 카테고리의 다른 글

절차지향 vs 객체지향  (0) 2022.11.04
[Java 8] Metaspace  (0) 2022.10.18
[Java 8] CompletableFuture  (0) 2022.10.13
[Java 8] Date와 Time  (0) 2022.10.12
[Java 8] Optional  (0) 2022.10.07

자바 Concurrent 소개

Concurrent 소프트웨어

  • 동시에 여러 작업을 할 수 있는 소프트웨어
  • Ex) 웹 브라우저로 유튜브를 보면서 키보드로 문서에 타이핑을 할 수 있다.

 

자바에서 지원하는 Concurrent 프로그래밍

  • 멀티프로세싱 (ProcessBuilder)
  • 멀티쓰레드

 

자바 멀티쓰레드 프로그래밍

  • Thread / Runnable

 

Thread 상속

    public static void main(String[] args) {
        HelloThread helloThread = new HelloThread();
        helloThread.start();
        System.out.println("Main : " + Thread.currentThread().getName());
    }

    static class HelloThread extends Thread {
        @Override
        public void run() {
            System.out.println("Thread : " + Thread.currentThread().getName());
        }
    }

Main이 먼저 출력 될 수도 있고, Thread가 먼저 출력될 수도있다. 

 

Runnable 구현 또는 람다

Thread thread = new Thread(() -> System.out.println("Thread : " + Thread.currentThread().getName()));
thread.start();
System.out.println("Main : " + Thread.currentThread().getName());

Runnable 또한 Main이 먼저 출력 될 수도 있고, Thread가 먼저 출력될 수도있다. 

 

Thread 주요 기능

  • 현재 쓰레드 멈춰두기(sleep)
    • 다른 쓰레드가 처리할 수 있도록 기회를 주지만 그렇다고 lock을 놔주진 않는다. (==>> 잘못하면 Deadlock (교착상태)에 빠짐)
  • 다른 쓰레드 깨우기 (Interrupt)
    • 다른 쓰레드를 꺠워서 InterruptedException을 발생시킨다. 그 에러가 발생했을 때 할 일은 코딩하기 나름. 종료시킬 수도 있고, 계속 하던일을 할 수도 있다.
  • 다른 쓰레드 기다리기 (join)
    • 다른 쓰레드가 끝날 때까지 기다린다.
public static void main(String[] args) throws InterruptedException {
    // sleep, interrupt
    Thread thread = new Thread(() -> {
        while(true) {
            System.out.println("Thread : " + Thread.currentThread().getName());

            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                // 자는 동안 누군가가 깨우면 error
                System.out.println("exit!!");
                return;
                //e.printStackTrace();
            }
        }
    });
    thread.start();

    System.out.println("Main : " + Thread.currentThread().getName());
    Thread.sleep(3000L);
    thread.interrupt();
    
    
    // join
    Thread thread = new Thread(() -> {
        System.out.println("Thread : " + Thread.currentThread().getName());

        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            // 자는 동안 누군가가 깨우면 error
            System.out.println("exit!!");
            return;
            //e.printStackTrace();
        }
    });
    thread.start();

    System.out.println("Main : " + Thread.currentThread().getName());
    thread.join();      // thread가 끝날 때까지 기다린 다음 아래 출력
    System.out.println(thread + " is finished");

}

Thread가 두개만 되도 복잡한데 Thread가 여러개면 어떻게 될까?? 상상만해도 너무 복잡할 것 같다..

 

 

 

Executors

고수준 Concurrency 프로그래밍

  • 쓰레드를 만들고 관리하는 작업을 애플리케이션에서 분리
  • 위의 기능을 Executors에게 위임

 

Executors가 하는 일

  • 쓰레드 만들기
    • 애플리케이션이 사용할 쓰레드 풀을 만들어 관리한다.
  • 쓰레드 생명 주기 관리
  • 작업 처리 및 실행
    • 쓰레드로 실행할 작업을 제공할 수 있는 API를 제공

ExecutorService를 사용한 코드
실행한 결과

실행한 결과를 보면, main쓰레드가 아닌 미리 만들어놓은 쓰레드 풀에서 가져온 것을 확인할 수 있다. 특이한 점은 shutdown을 해주기전까지 쓰레드는 종료되지 않는다(당연히 프로세스도 죽지 않음)는 점이다.

 

주요 인터페이스

  • Executor
    • execute(Runnable)
  • ExecutorService
    • Executor 상송 받은 인터페이스로, Callable도 실행할 수 있으며, Executor를 종료시키거나 여러 Callable을 동시에 실행하는 등의 기능을 제공
  • ScheduleExecutorService
    • ExecutorService를 상속받은 인터페이스로 특정 시간 이후에 또는 주기적으로 작업을 실행할 수 있다.

 

ExecutorService로 작업 실행하기

ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
    System.out.println("Hello :" + Thread.currentThread().getName());
});

 

ExecutorService로 멈추기

executorService.shutdown(); // 처리중인 작업 기다렸다가 종료
executorService.shutdownNow(); // 당장 종료

 

ScheduledExecutorService로 5초 뒤에 작업 실행하기

ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
scheduledExecutorService.schedule(()-> System.out.println(Thread.currentThread().getName()),
        5, TimeUnit.SECONDS);
scheduledExecutorService.shutdown();

 

Fork/Join 프레임워크

  • ExecutorService의 구현체로 손쉽게 멀티 프로세서를 활용할 수 있게끔 도와준다.

 

지금까지 사용한 Runnable은 기본적으로 return이 void로 반환 값이 없다. 만약, 반환을 받고싶으면 Callable을 사용해야 한다.

 

 

Callable과 Future

Callable

  • Runnable과 유사하지만 작업의 결과를 받을 수 있다. 

Future

  • 비동기적인 작업의 현재 상태를 조회하거나 결과를 가져올 수 있다.

 

결과를 가져오기 get()

ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> helloFuture = executorService.submit(() -> {
    Thread.sleep(2000L);
    return "Callable";
});
System.out.println("Hello");
String result = helloFuture.get();	//결과 값을 가져올 때까지 기다림  ==>> 블록킹
System.out.println(result);
executorService.shutdown();
  • 블록킹 콜이다.
  • 타임아웃(최대한으로 기다릴 시간)을 설정할 수 있다.

 

작업 상태 확인하기

  • isDone()
    • 완료했으면? true:false를 return

 

작업 취소하기

  • cancel()
    • 취소했으면? true:false를 return
    • parameter로 true를 전달하면 현재 진행중인 쓰레드를 interrupt하고 그러지 않으면 현재 진행중인 작업이 끝난 뒤 취소한다.

 

여러 작업 동시에 실행하기

  • invokeAll()
    • 동시에 실행한 작업 중에 제일 오래 걸리는 작업만큼 시간이 걸린다.

 

여러 작업 중에 하나라도 먼저 응답이 오면 끝내기 

  • invokeAny()
    • 동시에 실행한 작업 중에 가장 짧게 걸리는 작업만큼 시간이 걸린다.
    • 블록킹 콜이다.

 

 

CompletableFuture

자바에서 비동기(Asynchronous) 프로그래밍을 가능하게하는 인터페이스

  • Future를 사용해서 어느정도 가능했지만 하기 힘들 일들이 많다고 한다.

 

Future로 하기 어렵던 작업

  • Future를 외부에서 완료시킬 수 없다. 취소하거나 get()에 타임아웃을 설정할 수는 있따.
  • 블로킹 코드인 get()을 사용하지 않고서는 작업이 끝났을 때 콜백을 실행할 수 없다.
  • 여러 Future를 조합할 수 없다.
    • Ex) Event 정보 가져온 다음 Event에 참여하는 회원 목록 가져오기
  • 예외 처리용 API를 제공하지 않는다.

 

CompletableFuture

 

비동기로 작업 실행하기

  • 리턴 값이 없는 경우
    • runAsync()
  • 리턴 값이 있는 경우
    • supplyAsync()
  • 원하는 Executor(쓰레드풀)를 사용해서 실행할 수도 있다.
    • 기본은 ForkJoinPool.commonPool()
// 리턴 X
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    System.out.println("Not Return : " + Thread.currentThread().getName());
});
future.get();       // Not Return : ForkJoinPool.commonPool-worker-3

// 리턴 O
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
    return "Return : " + Thread.currentThread().getName();
});
System.out.println(future2.get());      // Return : ForkJoinPool.commonPool-worker-3

 

콜백 제공

  • thenApply(Function)
    • 리턴 값을 받아서 다른 값으로 바꾸는 콜백
  • thenAccept(Consumer)
    • 리턴 값을 또 다른 작업을 처리하는 콜백 (리턴 X)
  • thenRun(Runnable)
    • 리턴 값 받고 다른 작업을 처리하는 콜백
  • 콜백 자체를 또 다른 쓰레드에서 실행할 수 있다.
// 리턴 값을 받아서 다른 값으로 바꾸는 콜백
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return "Return : " + Thread.currentThread().getName();
}).thenApply((s) -> {   // 이것이 future와 다른 점
    System.out.println(s);  // Return : ForkJoinPool.commonPool-worker-3
    return s.toUpperCase();
});
System.out.println(future.get());   // RETURN : FORKJOINPOOL.COMMONPOOL-WORKER-3


// 리턴 값을 또 다른 작업을 처리하는 콜백 (리턴 X)
CompletableFuture<Void> future2 = CompletableFuture.supplyAsync(() -> {
    return "Return : " + Thread.currentThread().getName();
}).thenAccept((s) -> {
    System.out.println(s.toUpperCase());  // Return : ForkJoinPool.commonPool-worker-3
});

// 리턴 값 받고 다른 작업을 처리하는 콜백
CompletableFuture<Void> future3 = CompletableFuture.supplyAsync(() -> {
    return "Return : " + Thread.currentThread().getName();
}).thenRun(() -> {
    System.out.println("thenRun");
});

 

조합하기

  • thenCompose()
    • 두 작업이 서로 이어서 실행하도록 조합
  • thenCombine()
    • 두 작업을 독립적으로 실행하고 둘 다 종료 했을 때 콜백 실행
  • allOf()
    • 여러 작업을 모두 실행하고 모든 작업 결과에 콜백 실행
  • anyOf()
    • 여러 작업 중에 가장 빨리 끝난 하나의 결과에 콜백 실행

 

예외처리

  • Exceptionally(Function)
  • handle(BiFunction)
// exceptionally
boolean throwError = true;
CompletableFuture<String> hello = CompletableFuture.supplyAsync(() -> {
    if (throwError) {
        throw new IllegalArgumentException();
    }

    System.out.println("Hello " + Thread.currentThread().getName());
    return "Hello";
}).exceptionally(ex -> {
    return "Error!";
});

System.out.println(hello.get());

// handle
boolean throwError2 = true;
CompletableFuture<String> hello2 = CompletableFuture.supplyAsync(() -> {
    if (throwError2) {
        throw new IllegalArgumentException();
    }

    System.out.println("Hello " + Thread.currentThread().getName());
    return "Hello";
}).handle((result,ex) -> {
    if(ex != null) {
        return "Error!";
    }
    return result;
});

System.out.println(hello2.get());

 

 

 

'JAVA' 카테고리의 다른 글

[Java 8] Metaspace  (0) 2022.10.18
[JAVA 8] ParallelSort  (0) 2022.10.15
[Java 8] Date와 Time  (0) 2022.10.12
[Java 8] Optional  (0) 2022.10.07
[JAVA 8] Stream  (0) 2022.10.06

+ Recent posts