랜덤 포레스트(Random Forest)는 앙상블 학습 방법의 일종으로, 여러 개의 결정 트리를 학습시키고 그들의 예측을 결합하여 작동. 이 방식은 개별 트리의 예측의 정확도를 향상시키며, 과적합을 방지할 수 있다.
단계1. 부트스트랩 샘플링 (Bootstrap Sampling):
랜덤 포레스트는 각 트리를 학습시키기 위해 부트스트랩 샘플을 생성.
(Bootstrap"은 통계학에서 무작위로 샘플을 복원 추출하는 방법을 의미)
배깅의 기본 아이디어
부트스트랩 샘플은 원본 데이터 세트에서 중복을 허용하여 무작위로 선택된 샘플로 구성되며, 데이터개수는 원본의 크기를 유지.
부트스트랩이란 용어에 대해서는 다음을 참고하자.
"Bootstrapping"이라는 용어는 오래된 서양 표현 "to pull oneself up by one's bootstraps"에서 비롯되었습니다. 이 표현은 불가능한 일을 수행하려는 노력을 의미하며, 원래는 물리적으로 자신의 부츠 끈(bootstraps)을 당겨서 자신을 공중에 띄우는 것이 불가능하다는 것을 나타내기 위해 사용되었습니다. 하지만 시간이 지나면서, 이 표현은 더 긍정적이고 상징적인 의미로 발전하게 되었습니다. 이제 "to pull oneself up by one's bootstraps"는 개인이 자신의 노력과 자원으로 어려운 상황을 극복하고 성공을 달성할 수 있음을 나타냅니다. 이는 무엇인가를 시작하거나 개선하기 위해 외부 도움 없이 자신의 능력과 자원을 사용하는 것을 의미합니다.웹 개발과 통계학에서 "Bootstrapping"의 사용은 이러한 개념을 반영합니다:
웹 개발의 부트스트랩:웹 개발 분야에서 부트스트랩은 개발자가 기본 구조와 디자인을 빠르게 설정하고, 프로젝트를 더 빠르게 시작하고 진행할 수 있도록 돕는 프레임워크입니다. 이는 개발자가 외부 디자인 팀이나 추가 자원 없이도 효과적인 웹사이트를 구축할 수 있게 해줍니다.
통계학의 부트스트랩 샘플링:통계학에서 부트스트랩 샘플링은 원본 데이터셋만을 사용하여 통계적 추정을 수행하는 방법을 제공합니다. 이는 외부 데이터 또는 추가 정보 없이도 원본 데이터셋의 특성을 이해하고 분석할 수 있게 해줍니다.
이런 방식으로, "Bootstrapping"은 독립성과 자립성의 중요성을 강조하며, 제한된 자원으로도 무언가를 성취할 수 있음을 상징합니다.
단계2. 특성 무작위 선택:
데이터만 무작위 샘플링하는게 아니라, 날씨/온도/습도등 각 노드에서 분할을 수행하는 특성도 일부 특성만을 무작위로 선택하여 사용. 이 방식은 트리의 다양성을 증가시키며, 과적합을 방지
단계3. 단계1,2를 통해 생성된 다수의 결정 트리를 개별 학습
랜덤 포레스트는 위의 두 과정을 통해 여러 개의 결정 트리를 독립적으로 학습.
각 트리는 약간 다른 부트스트랩 샘플과 약간 다른 특성 집합을 사용하여 학습됨.
단계4. 앙상블을 통한 집계:
분류 문제의 경우, 랜덤 포레스트는 각 트리의 예측을 모아서 투표를 통해 최종 클래스 레이블을 결정. 회귀 문제의 경우, 랜덤 포레스트는 각 트리의 예측을 평균내어 최종 예측을 생성.
단계5. 아웃 오브 백 (Out of Bag) 평가:
중복을 허용하는 부트스르랩 샘플링의 특성에 의해 피전홀 원칙에 따라 일부 샘플은 특정 개별 트리 학습과정에서 제외된다(위의 예시에서는 #2번 샘플) 이러한 샘플을 사용하여 트리의 성능을 평가하고, 랜덤 포레스트의 전반적인 성능을 추정할 수 있다.
학습에 사용되지 않은 아웃 오브 백 샘플을 사용하여 각 트리의 성능을 평가하고, 이는 각 트리에 대한 오류율을 측정하는 데 사용될 수 있다. 랜덤 포레스트 평가: 모든 트리의 아웃 오브 백 오류율을 평균하여 랜덤 포레스트의 전반적인 아웃 오브 백 오류율을 계산. 이는 랜덤 포레스트 모델의 전반적인 성능을 추정하는 데 사용될 수 있다.
단계6. 일반적인 평가
랜덤 포레스트의 평가에 있어서 아웃 오브 백(Out of Bag, OOB) 평가는 선택적인 방법. 이는 별도의 검증 데이터셋을 필요로 하지 않으므로 유용할 수 있지만, 이는 랜덤 포레스트의 성능을 평가하는 유일한 방법은 아님. 실제로는, 다음과 같은 다양한 평가 방법들이 널리 사용됨
분할 검증 (Holdout Validation): 데이터를 학습 세트와 검증 세트로 분할하고, 학습 세트로 모델을 학습시킨 후 검증 세트로 모델의 성능을 평가.
교차 검증 (Cross-Validation): 데이터를 여러 개의 폴드로 분할하고, 각 폴드를 검증 세트로 사용하여 모델의 성능을 평가.
K번의 반복을 수행.
각 반복에서 하나의 폴드를 검증 세트로 선택하고, 나머지 K−1개의 폴드를 학습 세트로 사용
모델을 학습 세트로 학습시키고, 검증 세트로 모델의 성능을 평가.
이 방법은 모델의 성능을 보다 안정적으로 평가할 수 있음
부트스트랩 검증 (Bootstrap Validation): 여러 번의 부트스트랩 샘플을 생성하고, 각 샘플로 모델을 학습 및 검증하여 모델의 성능을 평가.
후자가 Resolver(리졸버)만 구현하면 스키마를 자동생성해주고 kotlin 타입과 연동되는등 kotlin을 사용한다면 더 편리한 측면이 있지만, 여기서는 전자를 사용한 기준으로 서술한다.
expediagroup 아티펙트의 경우, 내 경우엔 /graphql 404문제가 해결이 안돼서 springframework를 쓰기로 했다. 추가로 조사해보니 webmvc대신 webflux로 교체해야 호환되는 이슈도 있었다(여기) spring-boot-starter-web 대신 spring-boot-starter-webflux의존성으로 바꿔야 하는데 WebConfig.kt등 여러곳에서 코딩방식을 바꿔야 하는 걸로 보인다.
클라이언트 사이드(React/Next.js)
관련 library설치
npm install @apollo/client graphql
아폴로?
Apollo Client는 JavaScript 어플리케이션에서 GraphQL API와 통신할 수 있게 해주는 라이브러리입니다.
Facebook에서는 GraphQL을 발명했으며, Relay라는 GraphQL 클라이언트를 만들어 공개했습니다. 그러나 Apollo가 Relay보다 더 널리 사용되는 이유는 사용자 친화적: Apollo는 사용자 친화적이고, 초보자에게 친숙하며, 설정이 상대적으로 간단합니다. 문서화도 잘 되어 있어, 개발자들이 쉽게 접근하고 사용할 수 있습니다. 커뮤니티 지원: Apollo는 강력한 커뮤니티 지원을 받고 있으며, 다양한 추가 기능과 툴이 개발되고 있습니다. 또한 꾸준한 업데이트와 개선이 이루어지고 있어, 더 많은 개발자들이 Apollo를 선호하게 되었습니다.
src/main/resources/graphql/schema.graphqls 파일에 쿼리를 추가
type Query {
submissionCountByProblem(problemId: ID!): Int
}
2. 서비스 수정
SubmissionService에 getSubmissionCountByProblem매서드를 추가하여 문제별 제출 수를 가져옴
@Service
class SubmissionService(private val submissionRepository: SubmissionRepository) {
// 아래는 기존에 존재하던 매서드
fun submitProblem(userId: Long, problemId: Int, code: String): Submission {
val submission = Submission(
userId = userId,
problemId = problemId,
code = code,
status = "PENDING" // 초기 상태
)
return submissionRepository.save(submission)
}
// 아래 매서드 추가
fun getSubmissionCountByProblem(problemId: Int): Int {
return submissionRepository.countByProblemId(problemId)
}
}
3. 컨트롤러에 로직 추가
컨트롤러에@QueryMapping 어노테이션을 사용하여 submissionCountByProblem GraphQL쿼리를 처리하는 메서드를 추가(엔드포인트 추가는 아니지만 약간 유사)
package com.sevity.problemservice.controller
import ...
@RestController
class SubmissionController(private val submissionService: SubmissionService) {
// 기존 코드
// ...
@QueryMapping
fun submissionCountByProblem(@Argument problemId: Int): Int {
return submissionService.getSubmissionCountByProblem(problemId)
}
}
expediagroup 아티펙트의 경우, 위의 해결책을 적용해도 여전히 404가 떴고, 추가로 조사해보니 webmvc대신 webflux로 교체해야 하는 이슈가 있었다(여기) spring-boot-starter-web 대신 spring-boot-starter-webflux의존성으로 바꿔야 하는데 WebConfig.kt등 여러곳에서 코딩방식을 바꿔야 하는 걸로 보인다.
GraphQL은 서버 개발자에게 데이터 타입, 쿼리, 뮤테이션을 명시적으로 정의하도록 요구합니다. 이는 GraphQL 스키마에서 수행되며, 서버가 제공할 수 있는 데이터와 작업을 명확하게 정의합니다. 예를 들어, 서버 개발자는 사용자 데이터를 반환하는 getUser 쿼리와 사용자 데이터를 업데이트하는 updateUser 뮤테이션을 스키마에 정의할 수 있습니다.
클라이언트의 자유도 (Client Flexibility):
클라이언트는 서버에 정의된 스키마를 기반으로 데이터를 요청합니다.
필드 선택 (Field Selection):
클라이언트는 쿼리를 통해 필요한 필드만 선택할 수 있습니다. 예를 들어, 사용자 객체에 대한 쿼리를 만들 때, 클라이언트는 이름과 이메일만 요청할 수 있으며, 다른 필드는 무시할 수 있습니다. 이는 네트워크 트래픽을 최적화하고, 필요한 데이터만 가져오도록 할 수 있습니다.
중첩 및 복잡한 쿼리 (Nested and Complex Queries):
또한, 클라이언트는 중첩된 쿼리를 생성하여 관련된 객체와 필드를 한 번의 요청으로 가져올 수 있습니다. 이는 데이터의 관계와 구조를 유연하게 표현할 수 있으며, 복잡한 데이터 요청을 단순화할 수 있습니다.
응답 구조의 맞춤화 (Customized Response Structure):
클라이언트는 요청의 구조를 지정할 수 있으며, 서버는 클라이언트의 요청에 따라 응답을 제공합니다. 이는 클라이언트가 받고 싶은 데이터의 구조를 정확하게 지정할 수 있게 해줍니다.
type Query {
bookById(id: ID): Book
}
type Book {
id: ID
name: String
pageCount: Int
author: Author
}
type Author {
id: ID
firstName: String
lastName: String
}
장단점
Q&A
아니 무슨 단일 endpoint라고 자랑하더니 mutation마다 명세서를 만들어야 하는거 같네.(암튼 클라이언트 맘대로 쿼리하는 방임형 자유는 아닌거 같네) 이럴거면 endpoint 여러개인거랑 뭐가 다르지? Query의 경우는 개별 명세서가 필요 없나?
application.properties에서 포트설정해주고(이때 src/main/resources뿐 아니라 src/test/resources에 있는 파일도 해줘야함에 주의)
# in application.properties
grpc.server.port = 50051
다음처럼 gRPC서버 띄욱 listen작업도 해줘야 했다.
package com.sevity.authservice.config;
import ...
@Configuration
public class GrpcServerConfig {
private static final Logger logger = LoggerFactory.getLogger(GrpcServerConfig.class);
@Autowired
private SessionServiceImpl sessionService;
private Server server;
@Value("${grpc.server.port}")
private int port;
@PostConstruct
public void startServer() throws IOException {
server = ServerBuilder
.forPort(port)
.addService(sessionService) // Your gRPC service implementation
.build();
server.start();
logger.info("sevity gRPC server started on port {}", port);
logger.info("sevity gRPC service name: {}", sessionService.getClass().getSimpleName());
}
@PreDestroy
public void stopServer() {
if (server != null) {
server.shutdown();
}
logger.info("sevity gRPC server stopped");
}
}
gRPC는 Google이 개발한 고성능, 오픈 소스 및 범용의 원격 프로시저 호출(RPC) 프레임워크입니다.
효율적인 프로토콜로, 서버 간 통신에 아주 적합하며, Protocol Buffers를 사용하여 타입을 정의하고, 강력한 타입 검사와 높은 성능을 제공합니다. 이는 서로 다른 시스템 간에 통신을 가능하게 하며, 다양한 환경과 언어에서 작동합니다.
gRPC는 2015년 3월에 Google이 Stubby의 다음 버전을 개발하고 오픈 소스로 만들기로 결정했을 때 처음 생성되었습니다. gRPC의 최초 릴리스는 2016년 8월에 이루어졌습니다. 현재 gRPC의 최신 버전은 1.59.1(2023년 10월 6일기준)
장점:
기존의 REST등 텍스트 기반 프로토콜보다 더 효율적인 바이너리 프로토콜을 제공하여, 데이터 전송의 오버헤드를 줄이고 성능을 향상시킵니다.
컨트랙트 첫 접근 방식: 서비스의 인터페이스와 메시지를 먼저 정의하고, 이를 기반으로 코드를 생성합니다. (Protocol Buffers를 사용하여 데이터를 직렬화하고 역직렬화하여, 높은 성능을 제공합니다.)
스트리밍 및 빠른 통신: 양방향 스트리밍과 빠른 통신을 지원하여, 실시간 애플리케이션에 이상적입니다.
비동기 콜도 지원하는 것 같다.
단점:
복잡성: gRPC는 설정과 디버깅이 복잡할 수 있으며, 새로운 사용자에게 진입 장벽을 제공할 수 있습니다.
텍스트 기반 포맷의 부족: gRPC는 바이너리 프로토콜을 사용하므로, 텍스트 기반 프로토콜보다 디버깅이 어려울 수 있습니다.
브라우저 지원: gRPC-Web을 통해 브라우저에서 gRPC를 사용할 수 있지만, 네이티브 gRPC 클라이언트보다 기능이 제한적일 수 있습니다.
vs REST
REST는 HTTP/1.1을 기반으로 하며, 텍스트 기반의 JSON 또는 XML을 사용하여 데이터를 전송합니다. 이에 비해 gRPC는 HTTP/2를 기반으로 하며, 바이너리 기반의 Protocol Buffers를 사용합니다. gRPC는 REST보다 더 높은 성능과 더 낮은 데이터 오버헤드를 제공하지만, REST는 더 단순하고 더 넓게 지원됩니다.
현재 만들어 보고 있는 online judge 프로젝트의 서비스 구성은 다음과 같다.(관련 있는 2개만 표시. 실제로는 7개)
인증 서비스 (Backend): 사용자의 회원 가입, 로그인, 로그아웃, 세션 관리 등을 담당 인증 서비스 (Frontend): 사용자 인터페이스를 제공 (로그인 폼, 회원가입 폼 등)
한가지 알아두면 좋은점은 Spring Boot의 경우 /login, /logout endpoint의 경우 직접 정의하지 않아도 자동으로 처리한다는 점이다.(이점 때문에 디버깅시 많이 헷갈렸다ㅠ)
세션은 서버에서 브라우저로 set-cookie 헤더를 통해서 세션아이디를 부여한다.
아래처럼 브라우저 개발자 도구에서 확인가능하다(애플리케이션탭 > 쿠키섹션)
서버의 세션과 브라우저(클라이언트)의 쿠키 개념
세션을 서버에서 생성하고 세션id를 set-cookie를 통해서 브라우저(클라이언트)로 전달한다.
이때 쿠키는 브라우저를 종료해도 유지되는 지속쿠키를 쓰고 만료시점을 정의할수도 있고, 세션쿠키를 쓰면 브라우저 종료시 자동으로 쿠키도 삭제된다.
서버의 세션의 경우 지속시간을 application.properties에 다음과 같이 지정할 수 있다.
server.servlet.session.timeout=30m
현재 내가 테스트중인 프로젝트에서는 /login 성공시 세션쿠키가 발급되며, 세션 타임아웃의 효과는 확인이 안되었다.
일단은 이정도에서 더 깊이 안파고 넘어가기로 한다.
트러블슈팅
http에서 포트가 서로다른 서비스(MSA)간 연동하기
아래 크롬 설명에 따르면 https를 쓰고 secure 옵션을 주어야 SameSite=None으로 지정하면서 포트가 달라도 쿠키저장이 된다는것 같다.(관련글, 관련글2)
set-cookie로 응답을 제대로 했음에도 브라우저에 쿠키저장이 안될때
클라이언트에서 서버로 요청할때 credential을 보내줘야 했다(이것땜에 한참헤맴 ㅠ)
const response = await axios.post('https://sevity.com:9991/login', `username=${username}&password=${password}`, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
withCredentials: true, // 여기, 이 줄 없으면 브라우저에 쿠키저장 안된다!!
});
getSession(true)의 안전성
HttpSession session = request.getSession(false); 를 하면 존재하는 세션정보를 가져오고, false자리에 true를 넣으면 없으면 생성하라는 의미인데, 이렇게 하면 (내가만든세션), (SpringSecurity에 의해 아직 없지만 생성될 세션) 이렇게 두벌이 될까봐 우려했는데, 나중에 확인해보니 그렇지는 않았다. 따라서 항상 true로 호출해도 무방한 것 같다.
단 여기를 보면 멀티스레드 환경에서 레이스 컨디션에 의해 중복세션과 중복쿠키가 생성되는 경우는 있는 것 같다. 해결책은 아래처럼 동기화블록내에 생성과 처리를 묶어주면 되긴할듯(현재 내 구현에서는 그렇게 까지 하진 않았다)
synchronized (request) {
HttpSession session = request.getSession(true);
// ... 세션 사용 코드 ...
}
TMI
/login에서 반환되는 response.data 값과 SESSION쿠키의 값은 동일하나, SESSION쿠키의 경우 base64로 인코딩 되어 있다.
//response.data: c8545bb7-c90b-4d69-9128-08efd0a73866
//SESSION(쿠키): Yzg1NDViYjctYzkwYi00ZDY5LTkxMjgtMDhlZmQwYTczODY2
let encodedSessionId = 'Yzg1NDViYjctYzkwYi00ZDY5LTkxMjgtMDhlZmQwYTczODY2';
let decodedSessionId = atob(encodedSessionId);
console.log(decodedSessionId); // 출력: c8545bb7-c90b-4d69-9128-08efd0a73866
이는 TypeScript의 컴파일러와 유사한 역할을 수행하며, 코드에서 타입 오류를 찾아내는 데 도움이 됩니다.
예제: 먼저 mypy를 설치합니다:
pip install mypy
다음은 mypy를 사용한 Python 코드 예제입니다:(name에 대해 str이라는 어노테이션을 했음에 주목. python 3.5기능)
# 파일명: example.py
def greeting(name: str) -> str:
return "Hello, " + name
age: int = "25" # 이 줄은 타입 오류를 발생시킵니다.
print(greeting("Alice"))
터미널에서 mypy를 사용하여 코드를 검사합니다:
mypy example.py
이 명령을 실행하면, mypy는 age: int = "25"라인에서 타입 오류를 발견하고 이를 알려줍니다.
Pytype:
Pytype은 또 다른 Python 정적 타입 검사 도구.
어노테이션이 없어도 추론을 기반으로 동작함.
예를들어 아래와 같이 어노테이션이 없는 코드에 대해서 pytype을 수행하면
def multiply_numbers(a, b):
return a * b
result = multiply_numbers(5, '3')
print(result)
아래처럼 런타임전 경고를 확인할 수 있음(런타임에서는 Python은 문자열 '3'을 5번 반복하여 '33333'을 생성하고 오류로 판단하지 않음)
$ pytype example.py
Computing dependencies
Analyzing 1 sources with 0 local dependencies
ninja: Entering directory `/Users/user/.pytype'
[1/1] check example
FAILED: /Users/user/.pytype/pyi/example.pyi
pytype-single --imports_info /Users/user/.pytype/imports/example.imports --module-name example -V 3.8 -o /Users/user/.pytype/pyi/example.pyi --analyze-annotated --nofail --quick /Users/user/example.py
File "/Users/user/example.py", line 4, in <module>: unsupported operand type(s) for *: 'int' and 'str' [unsupported-operands]
Function multiply_numbers was called with the wrong arguments (1:15)
For more details, see https://google.github.io/pytype/errors.html#unsupported-operands.
ninja: build stopped: subcommand failed.
Mypy vs Pytype
Mypy
Mypy는 Dropbox에서 개발되었으며, Python의 첫 번째 정적 타입 검사 시스템으로 간주됩니다.
이 도구는 2012년부터 개발이 시작되었으며, 아직도 활발하게 개발이 진행되고 있습니다. Mypy는 standalone으로 실행되거나, 커맨드 라인이나 편집기 또는 IDE의 linter 통합의 일부로 작동할 수 있습니다. Mypy는 타입 어노테이션을 포함하지 않은 코드에 대해 대부분의 코드 검사를 수행하지 않으며, 이는 점진적으로 코드 기반을 주석 처리하고 있는 경우 Mypy가 시간을 낭비하지 않도록 하기 위함입니다.
Pytype
Pytype는 Google에서 개발되었으며, Mypy와 달리 타입 설명자 대신 추론을 사용합니다. 즉, Pytype는 코드 흐름을 분석하여 타입을 결정하려고 시도하며, 타입 어노테이션에 엄격하게 의존하지 않습니다1. Pytype는 가능한 한 관대하게 행동하며, 런타임에서 작동하고 어떤 어노테이션도 모순되지 않는 연산이 있으면 Pytype는 그것에 대해 불평하지 않습니다. pytype의 reveal_type(expr)을 사용해서 런타임 전에 type()과 비슷한 기능을 활용하여 디버깅/개발 가능