2025년 6월 18일 수요일

Docker로 설치된 PostgreSQL에 CSV 파일을 insert하는 과정


1. Docker 컨테이너 실행:
- PostgreSQL 컨테이너가 실행 중이 아니라면, 다음 명령어로 컨테이너를 시작

```
docker start <container_name>
```

2. 컨테이너에 CSV 파일 복사:
- 호스트 머신에 있는 CSV 파일을 Docker 컨테이너 내부로 복사

```
docker cp <csv_file_path> <container_name>:/home
```

3. 컨테이너 내부로 접속:
- 다음 명령어를 사용하여 실행 중인 PostgreSQL 컨테이너 내부로 접속

```
docker exec -it <container_name> bash
```

4. 데이터베이스 접속:
- 컨테이너 내부에서 PostgreSQL 데이터베이스에 접속

```
psql -U <username> -d <database_name>
```

5. 테이블 생성:
- CSV 파일의 데이터를 저장할 테이블을 생성, 테이블 구조는 CSV 파일의 컬럼과 일치해야 함

```sql
CREATE TABLE <table_name> (
column1 datatype,
column2 datatype,
...
);
```

6. CSV 파일 import:
- `COPY` 명령어를 사용하여 CSV 파일의 데이터를 테이블에 insert

```sql
COPY <table_name> FROM '/home/file.csv' DELIMITER ',' CSV HEADER;
```

- `DELIMITER` CSV 파일에서 사용된 구분자를 지정
- `CSV HEADER` CSV 파일의 첫 번째 행이 헤더(컬럼명)임을 나타냄

7. 데이터 확인:
- `SELECT` 문을 사용하여 데이터가 성공적으로 insert되었는지 확인

```sql
SELECT * FROM <table_name> LIMIT 10;
```

>테이블 구조를 CSV 파일과 일치시키는 것이 중요

==데이터베이스 안에서 직접 파일 실행

```sql
\i /home/user/insert.sql
```


2025년 6월 16일 월요일

페이징 API와 스크롤 API

// common/ScrollRequest.java
public record ScrollRequest(
String nextToken, // 다음 페이지 토큰
int size // 요청 크기
) {
public static ScrollRequest of(String nextToken, int size) {
return new ScrollRequest(nextToken, size);
}
}

// common/PageRequest.java
public record PageRequest(
int page, // 페이지 번호
int size, // 페이지 크기
String sortBy, // 정렬 기준
String direction // 정렬 방향
) {
public static PageRequest of(int page, int size, String sortBy, String direction) {
return new PageRequest(page, size, sortBy, direction);
}

public org.springframework.data.domain.PageRequest toPageRequest() {
return org.springframework.data.domain.PageRequest.of(
page,
size,
direction.equalsIgnoreCase("DESC") ?
Sort.by(sortBy).descending() :
Sort.by(sortBy).ascending()
);
}
}

// repository/PostRepository.java
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
// 페이징용 쿼리
@Query("""
select p
from Post p
join fetch p.writer
where p.deletedAt is null
""")
Page<Post> findAllPosts(Pageable pageable);

// 무한 스크롤용 쿼리
@Query("""
select p
from Post p
join fetch p.writer
where p.id < :lastPostId
and p.deletedAt is null
order by p.id desc
limit :size
""")
List<Post> findPostsForScroll(Long lastPostId, int size);
}

// service/PostService.java
@Service
@Transactional(readOnly = true)
public class PostService {
private final PostRepository postRepository;

// 페이징 방식
public PostListResponse findPostsByPaging(PageRequest request) {
Page<Post> posts = postRepository.findAllPosts(request.toPageRequest());
return PostListResponse.of(
posts.getContent().stream()
.map(PostResponse::from)
.toList(),
posts.getTotalElements(),
posts.getTotalPages(),
posts.hasNext()
);
}

// 무한 스크롤 방식
public ScrollPostResponse findPostsByScroll(ScrollRequest request) {
Long lastPostId = request.nextToken() != null ?
Long.parseLong(request.nextToken()) :
Long.MAX_VALUE;

List<Post> posts = postRepository.findPostsForScroll(lastPostId, request.size());

String nextToken = posts.size() == request.size() ?
String.valueOf(posts.get(posts.size() - 1).getId()) :
null;

return ScrollPostResponse.of(
posts.stream()
.map(PostResponse::from)
.toList(),
nextToken
);
}
}

// dto/PostListResponse.java
public record PostListResponse(
List<PostResponse> posts,
long totalElements,
int totalPages,
boolean hasNext
) {
public static PostListResponse of(
List<PostResponse> posts,
long totalElements,
int totalPages,
boolean hasNext
) {
return new PostListResponse(posts, totalElements, totalPages, hasNext);
}
}

// dto/ScrollPostResponse.java
public record ScrollPostResponse(
List<PostResponse> posts,
String nextToken
) {
public static ScrollPostResponse of(List<PostResponse> posts, String nextToken) {
return new ScrollPostResponse(posts, nextToken);
}
}


##### 구현된 기능 정리

1. 페이징 API
    - 전통적인 페이징 방식 구현
    - 전체 데이터 개수, 전체 페이지 수 제공
    - 정렬 기능 지원
    - 페이지 번호 기반 이동

2. 스크롤 API
    - 커서 기반 페이징 구현
    - nextToken을 사용한 다음 데이터 조회
    - 마지막 아이템 ID 기반 조회
    - 성능 최적화를 위한 LIMIT 사용

##### 주요 차이점

1. 데이터 접근 방식
    - 페이징: offset + limit 사용
    - 스크롤: 커서(lastPostId) 기반 조회
    
2. 반환 정보
    - 페이징: 전체 데이터 수, 전체 페이지 수 포함
    - 스크롤: 다음 조회를 위한 토큰만 포함

3. 성능 특성
    - 페이징: 데이터가 많을 경우 offset으로 인한 성능 저하 가능
    - 스크롤: 인덱스 기반 조회로 일관된 성능 보장

2025년 3월 10일 월요일

Node.js 서버의 동시 접속 한도 및 설정

Node.js 서버의 동시 접속 한도

Node.js 서버의 동시 접속 한도는 기본 설정으로 특정 숫자가 정해져 있지 않지만, 여러 요소에 의해 제한:

  1. HTTP 서버 기본 설정:

    • Node.js의 HTTP/HTTPS 서버는 기본적으로 연결 수에 엄격한 제한을 두지 않음
    • Express 같은 프레임워크는 기본 설정으로 대기열에 5,000개의 요청을 가질 수 있음
  2. 운영체제 제한:

    • 가장 일반적인 제한 요소는 운영체제의 최대 동시 열린 파일(소켓) 수
    • Linux에서는 보통 ulimit -n으로 확인 가능하며, 기본값은 1,024
    • 이는 서버가 1,024개의 동시 연결을 처리할 수 있음을 의미
  3. Node.js 이벤트 루프:

    • Node.js는 단일 스레드 이벤트 루프 모델을 사용하므로, 처리 능력은 CPU 작업 부하와 I/O 작업의 효율성에 따라 달라짐
  4. 하드웨어 리소스:

    • 메모리, CPU, 네트워크 대역폭 등 서버의 하드웨어 자원도 제한 요소

프로덕션 환경에서 더 많은 동시 연결 처리 방법

// 서버 생성 시 최대 연결 수 증가
const server = http.createServer(app);
server.maxConnections = 10000;

// OS 파일 디스크립터 한도 증가 (Linux/Unix)
// $ ulimit -n 10000 (터미널에서 실행)

// Keep-Alive 타임아웃 설정
server.keepAliveTimeout = 5000; // 5초
        

실제로는 클러스터 모듈이나 PM2 같은 프로세스 매니저를 사용하여 여러 개의 Node.js 인스턴스를 실행하고, Nginx나 HAProxy 같은 로드 밸런서를 앞단에 두는 것이 일반적

멀티파트 업로드 시스템의 경우, 특히 SSE(Server-Sent Events)를 사용한 진행률 모니터링 때문에 일반적인 HTTP 요청보다 더 많은 리소스를 사용할 수 있으므로, 프로덕션 환경에서는 서버 설정을 적절히 조정하는 것이 중요

현재 구조라면 로드밸런서(LB)만 추가해도 기본적으로 정상 동작할 수 있지만 대용량 트래픽과 멀티파트 업로드를 효율적으로 처리하기 위해서는 몇 가지 추가 설정이 필요

로드밸런서 추가 시 고려사항

  1. Connection 수 조정:

    • Nginx의 경우 worker_connections 값을 조정 (기본값은 보통 512나 1024)
    • HAProxy의 경우 maxconn 값을 조정
  2. 세션 관리:

    • 멀티파트 업로드의 경우 각 청크가 같은 서버로 가야 함
    • 특히 uploadId를 기준으로 동일 사용자의 요청이 동일 서버로 라우팅되어야 함
  3. 타임아웃 설정:

    • 대용량 파일 업로드는 일반 요청보다 오래 걸리므로 타임아웃 설정 확인 필요
    • Nginx의 경우 client_body_timeout, proxy_read_timeout 등을 조정

Nginx 설정 예시

http {
  # 워커 프로세스당 최대 연결 수 증가
  worker_connections 10000;

  # 요청 본문 크기 제한 증가 (대용량 청크 허용)
  client_max_body_size 10M;

  # 타임아웃 설정 증가
  client_body_timeout 120s;
  proxy_connect_timeout 120s;
  proxy_send_timeout 120s;
  proxy_read_timeout 120s;

  # 업로드 ID 기반 세션 일관성 설정
  upstream app_servers {
    ip_hash; # 또는 더 나은 방법으로 sticky sessions 사용
    server app1.example.com:3000;
    server app2.example.com:3000;
    # 추가 서버...
  }

  # SSE 관련 설정 (진행률 모니터링용)
  server {
    # ...

    location /api/progress {
      proxy_pass http://app_servers;
      proxy_http_version 1.1;
      proxy_set_header Connection '';
      proxy_buffering off;
      proxy_cache off;
      proxy_read_timeout 24h; # SSE 연결 유지를 위한 긴 타임아웃
    }

    # 일반 API 엔드포인트
    location /api {
      proxy_pass http://app_servers;
      proxy_http_version 1.1;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $host;
    }
  }
}
        

HAProxy 설정 예시

global
  maxconn 10000

defaults
  timeout connect 10s
  timeout client 30m # 클라이언트 타임아웃 증가
  timeout server 30m # 서버 타임아웃 증가

frontend http-in
  bind *:80
  default_backend app_servers

backend app_servers
  balance source # IP 기반 세션 고정
  option http-server-close
  server app1 app1.example.com:3000 check
  server app2 app2.example.com:3000 check
  # 추가 서버...
        

Node.js 서버에서도 고려할 점

로드밸런서를 추가하더라도 Node.js 서버 자체에서도 다음 설정을 확인해야 함:

// app.js 또는 서버 설정 파일
const server = http.createServer(app);

// 연결 시간 제한 증가
server.keepAliveTimeout = 60000; // 60초
server.headersTimeout = 65000; // 65초

// Express나 다른 미들웨어 설정
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
        

현재 코드에서는 SSE를 사용한 업로드 진행 모니터링이 있으므로, 특히 이 부분이 로드밸런서를 통해 제대로 작동하는지 확인하는 것이 중요. 로드밸런서는 SSE 연결을 장시간 유지할 수 있도록 설정

로드밸런서만 추가해도 기본적인 동작은 가능하지만, 효율적인 운영을 위해서는 로드밸런서와 Node.js 서버 모두에서 연결 수, 타임아웃, 세션 지속성 등의 설정을 적절히 조정해야 함

타임아웃 설정시 일관성이 필요. SSE 연결이 제대로 유지되려면 Node.js 서버의 타임아웃도 로드밸런서의 타임아웃과 일치하거나 그보다 길어야 함

// Node.js 서버 설정
const server = http.createServer(app);

// SSE 연결을 위한 긴 타임아웃 설정
server.keepAliveTimeout = 86400000; // 24시간 (밀리초 단위)
server.headersTimeout = 86460000; // 약간 더 길게 설정 (24시간 + 1분)
        

현재 구조에서는 다음과 같은 타임아웃 설정이 일관성 있게 적용되어야 함:

  1. 로드밸런서(Nginx/HAProxy): 24시간 타임아웃
  2. Node.js 서버: 24시간 타임아웃
  3. 업로드 상태 관리: 코드에서 expiresIn: 6 * 60 * 60 (6시간)으로 설정되어 있음

특히 uploadStateService에서는 비활성 상태에 대한 타임아웃이 6시간으로 설정되어 있는데, 이 시간도 일관성 있게 조정하는 것이 좋음

리소스 관리 측면에서는 실제 사용 패턴에 따라 이 타임아웃 값을 조정해야 함. 대부분의 업로드가 몇 시간 내에 완료된다면, 24시간보다 짧게 설정하는 것이 리소스 낭비를 줄일 수 있음

Docker로 설치된 PostgreSQL에 CSV 파일을 insert하는 과정

1. Docker 컨테이너 실행 : - PostgreSQL 컨테이너가 실행 중이 아니라면 , 다음 명령어로 컨테이너를 시작 ``` docker start <container_name> ``` 2. 컨테...