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시간보다 짧게 설정하는 것이 리소스 낭비를 줄일 수 있음

2013년 4월 3일 수요일

CUBRID 설치


환경 : CentOS release 5.8 (Final) / 2.6.18-308.20.1.el5

일단 홈페이지 다운로드 페이지로 접속한다
http://www.cubrid.com/zbxe/download

현재 날짜 기준으로 CUBRID-9.1.0.0212-linux.x86_64.sh 이 최신 버전

적당한 곳에 다운로드를 하고 설치할 그룹과 계정을 생성한다 (여기서는 cubrid/cubrid 로 사용)

그리고 다운로드 받은 파일을 cubrid 계정으로 그냥 실행
cubrid>$ sh CUBRID-9.1.0.0212-linux.x86_64.sh

위에서부터 차례대로

라이센스에 동의하냐?
기본 디렉토리에 설치할꺼냐?
CUBRID는 같은 버전끼리 호환된다. 알아들었냐?

그리고 위의 메세지가 나오면 설치가 성공적으로 끝난거다

설치가 끝난 후 다음 명령어를 실행한다
cubrid>$ . /home/cubrid/.cubrid.sh
CUBRID관련 환경정보 설정이므로 설치 후 한번만 실행하면 된다

모든 설치 및 설정이 완료되면 다음과 같이 구동 및 확인을 한다


웹매니저는 서버 설치시 자동 설치되니 방화벽 확인후 https://localhost:8282/ 로 접속을 하면 된다


일단 간단하게나마 설치를 했으니 매니저, 쿼리 브라우저를 설치하여 테스트를 해본다

테스트의 결론은..

큐브리드에 입사하고 싶다...... 하지만 능력이...ㅜㅜ



2013년 3월 28일 목요일

linux에 java 설치

환경 : CentOS release 5.5 (Final)

처음 centos를 설치하면 기본적으로 openjdk가 설치되어 있다
하지만 개발 환경이 sunjdk라면 해당 jdk를 따로 설치해야된다

일단 oracle 다운로드 페이지에서 설치파일을 다운로드 받자
http://www.oracle.com/technetwork/java/javase/downloads/index.html

현재시간으로 javase6의 가장 최신버전을 다운로드 받았다

1. 실행파일로 모드 변경
# chmod 755 jdk-6u41-linux-x64.bin 

2. java 설치
# ./jdk-6u41-linux-x64.bin 

3. 적당한 위치로 이동
# mv jdk1.6.0_41/ /usr/local/

4. 현재 java 버전 확인
# java -version

5. 현재 java 위치 확인
# which java

6. java 링크 경로 확인
# la -l /usr/bin/java (5번결과)

7. 기존링크 끊어주고 새로운 버전 링크지정
# unlink /usr/bin/java
# ln -s /usr/local/jdk1.6.0_41/bin/java /usr/bin/java (5번결과)

8. 최종확인
# ls -l /usr/bin/java
# java -version

9. symbolic link 관리
# cd /etc
# update-alternatives --install "/usr/bin/java" "java" "/usr/local/jdk1.6.0_41/bin/java" 1
# update-alternatives --config java
# java -version

9번과정은 안해도 되는건데 혹시나 8번까지 완료를 하고 나서도 java version이 openjdk로 인식할 경우 해주면 된다

2013년 3월 11일 월요일

centos에서 yum으로 mysql5.5 설치하기


환경 : CentOS release 5.8 (Final)


CentOS 5.x 에서 yum으로 설치할수 있는 mysql은 5.0.x 이다.

yum으로 mysql 5.5 버전을 설치하기 위해서는 다음 순서대로 실행하면 된다.


1. mysql 중지

service mysqld stop 

2. yum repository 추가

rpm -Uvh http://repo.webtatic.com/yum/centos/5/latest.rpm 

3. 기존 mysql 삭제

yum remove mysql mysql-* 

4. libmysqlclient15 설치

yum install libmysqlclient15 --enablerepo=webtatic 

5. mysql 5.5 설치

yum install mysql55 mysql55-server --enablerepo=webtatic 

6. mysql 시작

service mysqld start

7. 테이블 upgrade
mysql_upgrade -u root -p

처음엔 최신버전인 5.6을 설치하려고 mysql56을 했지만 실패
그냥 5.5 설치했다





2013년 3월 8일 금요일

jetty embedded + spring + spring-security + spring-data-jpa + mysql

환경
org.eclipse.jetty : 8.1.8.v20121106
org.springframework : 3.2.0.RELEASE
org.springframework.data : 1.2.0.RELEASE
org.springsecurity : 3.1.3.RELEASE
mysql : 5.5.28

STORY
web.xml, spring.xml 및 각종 config.xml을 없애고 java code 및 java annotation 으로 처리하는것이 최종 목적.
그와 함께 tomcat 대신 jetty를 임베디드 시켜 application 실행시키듯이 main method로 서버실행.
부가적으로 인증은 spring-security를 사용하고 db접근은 spring-data-jpa를 이용하였다

TEST
1. database-context.xml에서 db 설정을 한다 
   - database schema만 생성시켜 놓는다
   - 테이블이 없다면 서버 실행시 테이블을 자동생성 시킴
2. com.dorothy.jetty.Main - main을 실행시킨다
3. test user 정보를 db에 생성시킨다
4. restAPI를 호출한다
POST /test/{pathVar} HTTP 1.1
Host: localhost:8000
Authorization : access_id:access_key
5. log를 확인한다

TODO
처음부터 최종목표대로 NoXML로 하고 싶었지만 은근 애를 먹었다
그래서 일단은 xml설정으로 테스트를 시작하였다
이 소스를 가지고 NoXML로 바꿔나가야겠다

PS.
아 근데 구글블로그는 파일 첨부가 안되는거였군.....ㅡㅡ;;;
일단은 구글드라이브에..

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

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