volatile은 자바에서 멀티스레딩 환경에서 자주 사용되는 키워드로, 변수에 대한 가시성(visibility)을 보장해 주는 기능


volatile이란?

volatile 키워드는 변수 선언 앞에 붙여서 사용되며, 해당 변수가 여러 스레드에서 동시에 접근될 수 있음을 나타냅니다.


private volatile boolean running = true;

필요한 상황

자바에서 멀티스레딩을 할 때, 각 스레드는 자신만의 캐시에 변수 값을 저장할 수 있음

이때, 한 스레드가 변경한 값이 다른 스레드에 보이도록 캐시 업데이트 진행

예시:

class Example implements Runnable {
    private boolean running = true;

    public void run() {
        while (running) {
            // do something
        }
    }

    public void stop() {
        running = false;
    }
}

이 코드는 stop() 메서드에서 runningfalse로 바꾸더라도, run() 내부의 스레드는 true만 계속 보고 멈추지 않음


private volatile boolean running = true;

volatile의 효과

  1. 가시성 보장 (Visibility)
    • 어떤 스레드가 값을 바꾸면, 다른 모든 스레드는 그 변경을 즉시 볼 수 있습니다.
  2. 원자성은 보장하지 않음 (No Atomicity)
    • volatile읽기/쓰기 자체는 원자적이지만, 복합 연산(++, += 등)은 원자적이지 않아요.
    • 예: count++volatile로도 안전하지 않습니다.

volatile vs synchronized

특징 volatile synchronized
가시성 O O
원자성 X O
성능 빠름 상대적으로 느림 (락 걸림)
용도 단순 플래그, 읽기/쓰기 변수 복잡한 상태 공유, 연산 등

🔸 사용 예시

public class FlagExample {
    private volatile boolean flag = false;

    public void setFlagTrue() {
        flag = true;
    }

    public void waitForFlag() {
        while (!flag) {
            // 대기 중
        }
        System.out.println("Flag detected!");
    }
}

요약

  • volatile은 멀티 스레드 환경에서 변수 값의 변경이 다른 스레드에 보이지 않는 문제(가시성 문제)를 해결하기 위해 사용된다.
  • 일반적으로 CPU는 변수 값을 자신의 캐시에 저장하고 사용하기 때문에, 한 스레드에서 변경한 값이 다른 스레드의 캐시에 반영되지 않아 변경이 전파되지 않는 문제가 발생할 수 있다.
  • volatile 키워드를 사용하면, 변수의 값을 항상 메인 메모리에서 읽고 쓰게 되어 최신 값이 모든 스레드에 보이도록 보장할 수 있다.

  • Thread A는 여전히 true인 줄 알고 계속 루프를 돔
  • Thread B는 메모리에서 false로 바꿨지만, A는 캐시된 값을 사용 중

  • Thread B가 false로 바꾸면
  • Thread A도 즉시 최신 값을 보고 루프 종료
반응형

https://spody.tistory.com/216

https://spody.tistory.com/217

두 글을 정리요약했습니다.

발생 현상

  • AUIGrid(자바스크립트 라이브러리)를 사용하는 엑셀 다운로드 시 깨진 파일을 다운받음
  • 그러나 백엔드 서버에서 해당 파일을 저장해둔 위치에 가서 파일을 열어보면 정상적으로 열리는 상태
  • 다운로드 받은 파일(깨진 파일)과 정상 파일을 메모장으로 열어서 데이터 비교했을 때 파일이 쓰이다 만 것처럼 데이터가 들어가 있음 (깨진 파일은 데이터가 중간에 끊김)

발생 원인

  • 파일이 중간에 깨진 이유는 디스크 쓰기 작업이 완료(flush)되기 전에 파일을 반환했기 때문에 발생
  • 백엔드 서버의 로직을 봤을 때 해당 코드는 동기적으로 처리되어야 정상인데 왜 문제가 발생했는가?
    • AIP 서버에서 파일 데이터를 수신하면, 해당 데이터는 먼저 메모리 내 커널 페이지 캐시에 저장
    • 이 시점에서 쉘 스크립트 또는 PHP는 파일 쓰기 작업이 완료된 것으로 간주하고, 바로 다음 단계로 넘어감
    • 하지만 flush가 디스크에 완전히 반영되기 전 프론트엔드에서 해당 파일을 다운로드 받아 문제가 발생
    • 즉, 쓰기 지연(write-back) 상태에서 read가 먼저 발생해 깨진 파일을 받게 됨

해결 방안

  • 3초 딜레이 적용
    • AUIGrid에서 사용하는 백엔드 호출 로직(DrmService.php)에 sleep(3)을 추가하여 디스크 flush가 완료되도록 유도
  • 쉘 스크립트 내에서 파일 디스크 flush 유도
    • 반환받은 파일 데이터를 .tmp 확장자로 저장한 후, mv file.xlsx.tmp file.xlsx 방식으로 최종 이름 변경
    • mv는 atomic rename 동작이기 때문에 flush 유도에 효과적
  • Python fsync() 시스템콜 직접 호출
    • python3 -c "f = open('${ORIGINAL_PATH}', 'rb+'); import os; os.fsync(f.fileno()); f.close()"
    • 명시적으로 flush 요청을 보냄 (단, NFS 환경에서는 보장되지 않을 수 있음)

고려 사항

  • 해당 문제는 운영 환경에서만 발생하며, 개발 환경에서는 재현되지 않음
  • 운영 서버에서 직접 코드를 수정하거나 임시 로직을 삽입해 테스트할 필요가 있음
  • 백엔드에서 생성하는 다른 엑셀 암호화 파일들도 동일한 문제의 가능성이 있음
    • 단, 해당 로직은 후처리 과정이 존재해 자연스러운 딜레이가 발생했기 때문에 flush가 완료되었음
  • Content-Length 기반 .done + mv 전략은, 상대 서버가 전송하는 파일이 반드시 바이너리 형식이며 Content-Length를 정확하게 명시한다는 전제를 필요로 한다. 하지만 상대 서버가 우리가 직접 구현한 것이 아니라면, 전송 형식이 base64, gzip, chunked encoding 등으로 바뀌거나 Content-Length가 부정확할 수 있기 때문에, 이 전략은 실질적으로 적용이 불가능하다고 판단하여 제외함.

NFS(Network File System)를 곁들인 저장소

쉘 스크립트나 Python을 통해 flush를 직접 유도했음에도 불구하고, 운영 환경에서는 여전히 깨진 파일이 다운로드되었음.

이를 확인한 결과, 해당 파일이 저장된 위치는 NFS 마운트 스토리지였다.

NFS 환경 특성

  • NFS export 옵션 중 sync, async가 있음
  • 옵션이 명시되지 않으면 기본값은 async → flush 지연

NFS export 옵션 설명

  • sync:
    • 클라이언트의 write() 요청마다 NFS 서버가 디스크에 즉시 flush한 후 OK 응답 반환
    • 안정성 높음, 성능 낮음
  • async (기본값):
    • write() → 클라이언트 커널 캐시에 저장 → 일정 시간 후 NFS 서버에 전송
    • 서버에서도 메모리에 저장 후 나중에 flush (2단계 지연)
    • fsync() 호출 → 클라이언트는 COMMIT 요청을 보내지만, 서버 flush 시점은 커널이 결정
    • 이로 인해 flush 전에 장애 발생 시 데이터 손실 가능성 있음

NFS 환경에서 flush 요약

  • 백엔드 서버는 NFS 마운트 스토리지에 파일을 저장하고 있음
  • NFS의 async 설정 때문에 flush 요청이 디스크에 즉시 반영되지 않음
  • flush()나 fsync(), mv 등을 해도 NFS 서버의 커널 스케줄러가 flush 시점을 결정하기 때문에 클라이언트 입장에서 flush가 즉시 완료되었는지는 보장되지 않음

최종 해결 방안

  • NFS export 옵션을 sync로 바꾸는 건 성능 리스크가 커서 적용 어려움
  • 엑셀 파일은 사용자가 다운로드 받기만 하면 되므로, 3초 대기 정도는 큰 무리가 없음
  • 따라서 엑셀 path를 반환하기 전 sleep 3을 넣는 것이 현재 환경에선 가장 현실적인 대응
반응형

'LTF(learn through failure)' 카테고리의 다른 글

파일 flush 문제 공유 (NFS)  (0) 2025.04.13
파일 flush 문제 공유  (0) 2025.04.05

https://spody.tistory.com/216

직전에 작성한 글로부터 이어집니다.

NFS(Network File System)를 곁들인 저장소

쉘 스크립트나 Python을 통해 flush를 직접 유도했음에도 불구하고, 운영 환경에서는 여전히 깨진 파일이 다운로드되었음. 이를 확인한 결과, 해당 파일이 저장된 위치는 NFS 마운트 스토리지였다.

NFS 환경 특성

  • NFS export 옵션 중 sync, async가 있음
  • 옵션이 명시되지 않으면 기본값은 async → flush 지연

NFS export 옵션 설명

  • sync:
    • 클라이언트의 write() 요청마다 NFS 서버가 디스크에 즉시 flush한 후 OK 응답 반환
    • 안정성 높음, 성능 낮음
  • async (기본값):
    • write() → 클라이언트 커널 캐시에 저장 → 일정 시간 후 NFS 서버에 전송
    • 서버에서도 메모리에 저장 후 나중에 flush (2단계 지연)
    • fsync() 호출 → 클라이언트는 COMMIT 요청을 보내지만, 서버 flush 시점은 커널이 결정
    • 이로 인해 flush 전에 장애 발생 시 데이터 손실 가능성 있음

NFS 환경에서 flush 요약

  • 백엔드 서버는 NFS 마운트 스토리지에 파일을 저장하고 있음
  • NFS의 async 설정 때문에 flush 요청이 디스크에 즉시 반영되지 않음
  • flush()나 fsync(), mv 등을 해도 NFS 서버의 커널 스케줄러가 flush 시점을 결정하기 때문에 클라이언트 입장에서 flush가 즉시 완료되었는지는 보장되지 않음

최종 해결 방안

  • NFS export 옵션을 sync로 바꾸는 건 성능 리스크가 커서 적용 어려움
  • 엑셀 파일은 사용자가 다운로드 받기만 하면 되므로, 3초 대기 정도는 큰 무리가 없음
  • 따라서 엑셀 path를 반환하기 전 sleep 3을 넣는 것이 현재 환경에선 가장 현실적인 대응
반응형

'LTF(learn through failure)' 카테고리의 다른 글

파일 flush 문제 해결 전략  (1) 2025.04.13
파일 flush 문제 공유  (0) 2025.04.05

발생 현상

  • AUIGrid(자바스크립트 라이브러리)를 사용하는 엑셀 다운로드 시 깨진 파일을 다운받음
  • 그러나 백엔드 서버에서 해당 파일을 저장해둔 위치에 가서 파일을 열어보면 정상적으로 열리는 상태
  • 다운로드 받은 파일(깨진 파일)과 정상 파일을 메모장으로 열어서 데이터 비교했을 때 파일이 쓰이다 만 것처럼 데이터가 들어가 있음(깨진 파일은 데이터가 중간에 끊김)

발생 원인

  • 파일이 중간에 깨진 이유는 디스크 쓰기 작업이 완료(flush)되기 전에 파일을 반환했기 때문에 발생
  • 백엔드 서버의 로직을 봤을 때 해당 코드는 동기적으로 처리되어야 정상인데 왜 문제가 발생했는가?
    • AIP 서버에서 파일 데이터를 수신하면, 해당 데이터는 먼저 메모리에 존재하는 파일 스트림에 저장됨. 이 시점에서 쉘 스크립트는 파일 쓰기 작업이 완료된 것으로 간주(php 코드도 동일)하고, 바로 다음 단계로 넘어간다. 하지만 파일 스트림의 내용이 실제 디스크에 완전히 flush되기 전에 프론트에서 해당 파일을 다운로드받아 문제가 생기고 있었음
  • 엑셀 다운로드 흐름

해결 방안

  • 3초 딜레이 적용
    • AUIGrid에서 사용하는 백엔드 호출 로직(DrmService.php)에 sleep(3)을 추가하여 디스크 flush가 완료되도록 유도
  • 쉘 스크립트 내에서 파일 디스크 flush 유도
    • mv 명령어 사용 시 OS 스케줄러의 디스크 flush를 유도하는 커널 레벨 특성을 이용
    • 반환받은 파일 데이터를 .tmp 확장자로 저장한 후, mv file.xlsx.tmp file.xlsl 방식으로 이름 변경 처리
    • 이 과정을 통해 flush가 완료된 시점에만 최종 파일로 접근 가능하게 만듦
  • python flush() 시스템콜 직접 호출
    • 아래의 파이썬 호출 코드를 쉘 스크립트 에서 실행 python3 -c "f = open('${ORIGINAL_PATH}', 'rb+'); import os; os.fsync(f.fileno()); f.close()"

고려 사항

  • 해당 문제는 운영 환경에서만 발생하며, 개발 환경에서는 재현되지 않기 때문에 정확한 원인 분석이 제한적
  • 운영 서버에서 직접 코드를 수정하거나 임시 로직을 삽입해 테스트할 필요가 있음
  • 백엔드에서 생성하는 다른 엑셀 암호화 파일들도 같은 문제의 가능성이 존재
    • 다만, 해당 로직은 후처리 과정이 존재하기 때문에 자연스러운 딜레이가 발생해 flush가 완료되고, 문제 없이 다운로드가 가능했던 것으로 보임. 따라서 AIP 암호화 처리시 반드시 호출하는 쉘 스크립트 내에서 해결하는 것이 Best

요약

반응형

'LTF(learn through failure)' 카테고리의 다른 글

파일 flush 문제 해결 전략  (1) 2025.04.13
파일 flush 문제 공유 (NFS)  (0) 2025.04.13

목적

API Gateway의 목적은 클라이언트와 백엔드 서비스 간의 중간 계층 역할을 하면서 다양한 기능들을 수행하여 시스템의 효율성과 보안성을 높이는 것

특징

1. 단일 진입점 제공 (Single Entry Point)

  • 클라이언트는 여러 백엔드 서비스를 직접 호출하지 않고, API Gateway 하나만 호출하면 됨.
  • 마이크로서비스 아키텍처에서 특히 유용.

2. 요청 라우팅 (Request Routing)

  • 들어오는 요청을 적절한 백엔드 서비스로 라우팅
  • 예: /user → 사용자 서비스, /order → 주문 서비스

3. 보안 관리

  • 인증(Authentication), 인가(Authorization)를 중앙에서 처리
  • JWT 토큰 검사, API 키 체크 등

4. 로깅 및 모니터링

  • 요청 로그를 수집하거나, 트래픽 분석 등 관측 기능을 제공
  • 성능 모니터링, 트래픽 제어 등도 가능

5. 요청/응답 변환

  • 요청 또는 응답의 포맷을 변환 (예: XML ↔ JSON)
  • 백엔드는 단순하게 유지하고, 클라이언트 맞춤 응답 제공 가능

6. 로드 밸런싱 및 장애 처리

  • 여러 백엔드 인스턴스 사이에서 트래픽 분산
  • 장애 발생 시 다른 인스턴스로 우회

7. 속도 제한 및 트래픽 제어 (Rate Limiting)

  • 남용 방지를 위해 호출 빈도 제한
  • 과도한 요청에 대한 보호

8. 캐싱

  • 빈번한 요청 결과를 캐싱하여 응답 속도 개선 및 서버 부하 감소.

API Gateway는 다음과 같은 역할을 하며 시스템을 더 안전하고, 효율적이며, 확장 가능하게 도움을 준다.

API Gateway의 부하 분산

1. API Gateway가 부하 병목이 될 수 있는 이유

  • 모든 요청을 한 곳(API Gateway)으로 집결시키므로, 트래픽 집중이 발생.
  • 트래픽이 급증하면, API Gateway가 요청을 처리하는 데 시간이 오래 걸릴 수 있음.

2. 부하를 분산시키는 방법

2.1 수평 확장(Scaling Out)

  • API Gateway 서버를 여러 대 운영하고, 그 앞단에 로드 밸런서(Load Balancer) 를 두어 부하를 분산
  • 예: Nginx, AWS ALB, Google Cloud Load Balancing 등

2.2 캐싱(Caching) 활용

  • 자주 요청되는 결과를 캐싱하면, Gateway가 각 요청마다 백엔드까지 직접 호출할 필요가 없어짐
  • 응답 속도 향상 및 서버 부하 감소

2.3 비동기 처리(Async)와 메시지 큐(Message Queue)

  • 어떤 요청은 즉시 처리가 필요하지 않을 수 있습니다(예: 이메일 전송, 비동기 트랜잭션 등)
  • 메시지 큐(Kafka, RabbitMQ 등)를 통해 처리할 수 있도록 하여 Gateway가 동기 처리 부담을 완화

2.4 서버리스(Serverless) 사용

  • AWS API Gateway + Lambda 조합 등으로 필요한 시점에만 자동 확장이 가능하도록 설계
  • 초당 수백만 건의 요청도 일정 수준까지는 감당하도록 구성 가능

2.5 API Gateway 경량화

  • Gateway 레이어에 지나치게 많은 기능(인증/인가, 변환, 로깅 등)을 몰아넣으면 부하가 커지기에 잘 고려해야함
  • 꼭 필요한 기능만 Gateway에서 처리하고, 나머지는 별도 마이크로서비스 혹은 다른 계층에서 담당하게 하여 Gateway의 역할을 경량화

3. 단일 장애 지점을 피하는 고가용성(HA) 구성

  • API Gateway가 멈추면 모든 서비스가 사실상 중단될 가능성이 있음
  • 따라서, 멀티 AZ(가용 영역), 멀티 리전 또는 다중 Gateway 인스턴스 구성을 통해 장애 발생 시에도 서비스 중단이 최소화되도록 하는 구성이 필요

4. 결론

  • API Gateway가 모든 트래픽의 단일 진입점으로 동작하므로, 제대로 설계하고 확장 전략을 마련하지 않으면 병목 및 장애 지점이 될 수 있다.
  • 하지만 수평 확장, 캐싱, 비동기 처리, 고가용성 구성 등을 통해 충분히 분산 설계가 가능함
  • 즉, 잘못 설계하면 병목이 되지만, 올바른 인프라/아키텍처 구성과 모니터링을 통해 API Gateway의 이점(중앙 집중 관리, 보안, 모니터링 등)을 극대화하면서도 부하 문제 완화가능
반응형

'공부 > CS' 카테고리의 다른 글

HLS 프로토콜  (1) 2025.06.02
WAF, ZTN  (1) 2025.04.27
워터마크 기술 개념  (0) 2025.03.03
아키텍처 간략 정리  (0) 2025.01.05
언어별 연산 속도  (0) 2024.12.30

백준 포도주 시식

https://www.acmicpc.net/problem/2156

접근 방식

조건 : 연속으로 놓여 있는 3잔을 모두 마실 수는 없음

최초에 값을 초기화할 때 앞의 2개의 잔을 모두 마실 필요가 있나? x

마셨을 때 : 1

마시지 않았을 때 0

기준으로 표현하자면,

경우의 수는 110, 101, 011로 좁혀지고, 010은 고려대상이 아님


구현 방법

  • DP배열에서 위 접근방식을 적용하면 해결됨

  • 바텀업으로 값을 채워나감
  • 최초 값은 index 2까지 초기화
    • 0 : 1
    • 1 : 11
    • 2 : 101, 110, 011
  • 이후 점화식을 코드로 구현
    • i index 기준으로 봤을 때 각 경우의 수는 아래와 같이 표현됨
    • 110 : dp[i-1]
    • 101 : dp[i-2] + arr[i]
    • 011 : dp[i-3] + arr[i-1] + arr[i]

풀이

import java.io.*;
import java.util.*;

public class p2156 {
    static int[] arr;
    static int[] dp;

    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int n = Integer.parseInt(br.readLine());

        arr = new int[n];
        dp = new int[n];

        for(int i = 0; i < n; i++){
            arr[i] = Integer.parseInt(br.readLine());
        }

        if(n == 1){
            System.out.println(arr[0]);
            return;
        }

        if(n == 2){
            System.out.println(arr[0] + arr[1]);
            return;
        }

        dp[0] = arr[0];
        dp[1] = arr[0] + arr[1];
        dp[2] = Integer.max(dp[1],
                Integer.max(dp[0] + arr[2], // 101
                        arr[1] + arr[2])); // 011

        for(int i = 3; i < n; i++){
            dp[i] = Integer.max(dp[i-1], // 110
                    Integer.max(dp[i-2] + arr[i], // 101
                            dp[i-3] + arr[i-1] + arr[i])); // 011
        }

        System.out.println(dp[n-1]);
    }
}


후기

점화식을 세워서 진행하는 바텀업 DP 문제는 경우의 수를 어떻게 잘 구현 해야할지 머릿속에서 잘 구상해야하는데 이게 아직도 어려운 것 같다.

반응형

워터마크(Watermark)란?

워터마크(Watermark)는 디지털 콘텐츠(이미지, 영상, 오디오, 문서 등)에 저작권 보호, 출처 표기, 위변조 방지 등의 목적으로 삽입되는 표식이다. 워터마크는 가시적(Visible)비가시적(Invisible) 방식으로 나뉜다.


1. 가시적 워터마크 (Visible Watermark)

  • 정의

이미지, 영상, 문서 등에 눈에 보이는 형태로 삽입된 워터마크를 의미한다.

  • 특징
    • 사람이 직접 볼 수 있음 (예: 로고, 텍스트, 투명한 마크)
    • 저작권 보호 및 출처 표시 목적
    • 복제 방지를 위한 억제 효과
    • 그러나 이미지 편집 기술(포토샵 등)로 제거 가능
  • 예시
    • 사진에 삽입된 "© 2025 PhotographerName" 표시
    • 영상의 한쪽 구석에 있는 방송국 로고 (예: "KBS", "MBC")
    • PDF 문서의 배경에 투명하게 삽입된 회사명

2. 비가시적 워터마크 (Invisible Watermark)

  • 정의

사람의 눈에는 보이지 않지만, 디지털 신호(이미지, 영상, 오디오, 문서)에 숨겨진 형태로 삽입된 워터마크를 의미한다.

  • 특징
    • 사람이 직접 볼 수 없음 (특정 소프트웨어나 알고리즘을 통해 확인 가능)
    • 저작권 보호 및 원본 출처 확인
    • 변형, 편집 후에도 검출 가능 (강인성에 따라 다름)
    • 이미지, 오디오, 영상에 널리 사용됨
  • 기술적 기법
    • 공간 영역 기법: 픽셀값에 미세한 변화 삽입
    • 주파수 영역 기법: 푸리에 변환(DFT), 웨이브렛 변환(DWT) 등을 활용해 삽입
    • 양자화 지수 변조(QIM): 특정 패턴을 신호에 삽입하여 탐지
  • 예시
    • 사진 속 특정 픽셀에 디지털 서명 숨기기
    • 영화나 음악 파일에 저작권 정보 삽입
    • 문서에 특정한 패턴을 추가해 출처 확인

3. 가시적 vs 비가시적 워터마크 비교

비교 항목 가시적 워터마크 비가시적 워터마크
눈으로 보이는지 O (보임) X (보이지 않음)
저작권 보호 효과 높음 (사람들이 인식) 높음 (탐지 필요)
위변조 방지 제거 가능성 높음 강인한 방식은 제거 어려움
출처 추적 육안으로 확인 가능 디지털 분석 필요
사용 예시 사진 로고, 방송국 마크, PDF 배경 디지털 인증, 영상·음원 추적

결론

  • 가시적 워터마크는 저작권 보호를 강조하고 쉽게 알아볼 수 있는 장점이 있지만 제거될 가능성이 있다.
  • 비가시적 워터마크는 콘텐츠의 무결성을 유지하면서도 추적이 가능하지만, 분석 도구가 필요하다.
  • 두 가지 방법을 함께 사용하면 보안성과 저작권 보호 효과를 극대화할 수 있다.
반응형

'공부 > CS' 카테고리의 다른 글

WAF, ZTN  (1) 2025.04.27
API Gateway  (0) 2025.03.24
아키텍처 간략 정리  (0) 2025.01.05
언어별 연산 속도  (0) 2024.12.30
Context Switching(문맥 교환)  (0) 2024.09.19

백준 감소하는 수(줄어드는 수)

https://www.acmicpc.net/problem/1174

https://www.acmicpc.net/problem/1038

접근 방식

최초엔 n까지 범위의 수를 생각했는데, 잘못 생각한 것이였고, 앞자리부터 점점 감소하는 수의 모든 경우의 수 중 몇번째인지 n으로 입력받아 찾는 문제


구현 방법

  • 감소하는 수에 대해 모든 조합을 찾은 후 정렬해 수행
  • 이 때 최대 수의 범위까지 1씩 더해가며 찾는 것이 아닌, 값이 들어갈 수 있는 범위에 대해서만 찾기

  • 9876543210 까지가 최대 수
  • 재귀에서 자릿수(depth)를 받아서 depth 범위만큼 foreach돈다.
  • 기본적으로 for문 돌 때 자기보다 낮은 수에 대해서만 수행
  • 재귀 안에서 count가 같으면 return - count는 static으로 갖고있기에 종료가능
  • for문이 다 돌면 재귀호출 시행

풀이

package Baekjoon.gold;

import java.io.*;
import java.util.*;
import java.util.stream.Collectors;

public class p1174 {

    static Stack<Long> st = new Stack<>();
    static ArrayList<Long> al = new ArrayList<>();

    static int count = 0;
    static int n;
    static StringBuilder sb = new StringBuilder();

    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        n = Integer.parseInt(br.readLine());

        /*
         0, 1, 2, 3, 4, 5, 6, 7, 8, 9
         10 | 20, 21 | 30, 31, 32 | 40, 41, 42, 43 | 50, 51, 52, 53, 54 |
         60, 61, 62, 63, 64, 65 | 70, 71, 72, 73, 74, 75, 76 |
         80, 81, 82, 83, 84, 85, 86, 87 | 90, 91, 92, 93, 94, 95, 96, 97, 98 |
         210 |320, 321, 310 | 410, 420, 421, 430, 431, 432
         */

        //9 -> 8 7 6 5 4 3 2 1 0
        //8 -> 7 6 5 4 3 2 1 0
        //7 -> 6 5 4 3 2 1 0
        //6 -> 5 4 3 2 1 0
        //5 -> 4 3 2 1 0
        //4 -> 3 2 1 0
        //3 -> 2 1 0
        //2 -> 1 0
        //1 -> 0

        // 10 20 21 30 31 32
        //9876543210 까지가 최대 수
        //재귀에서 자릿수(depth)를 받아서 depth 범위만큼 foreach돈다.
        //기본적으로 for문 돌 때 자기보다 낮은 수에 대해서만 수행
        //재귀 안에서 count가 같으면 return - count는 static으로 갖고있기에 종료가능
        //for문이 다 돌면 재귀호출 시행

        if(n <= 10){
            System.out.println(n-1);
            return;
        }
        recur(10);
        List<Long> list = al.stream().sorted().collect(Collectors.toList());

        long result = -1;
        if(list.size() >= n){
            result = list.get(n-1);
        }

        System.out.println(result);
    }

    private static void recur(long nowNum){
        for(long i = 0; i < nowNum; i++){
            count++;
            st.push(i);
            st.forEach(sb::append);
            al.add(Long.valueOf(sb.toString()));
            sb.setLength(0);
            recur(i);
            st.pop();
        }
    }
}

후기

최초에 문제를 잘못 읽었고, 이후엔 전체 탐색을 효율적으로 하기위한 노력(가지치기)을 하는 데 시간을 많이 썼음

반응형

AtomicInteger.class의 메서드를 탐방하다 발견하게 된 native 키워드 탐방

@IntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);

native 키워드는 Java에서 네이티브 코드를 사용해 자바 코드로는 직접 수행하기 어려운 작업을 해결하기 위해 사용됨. Java는 기본적으로 플랫폼 독립성을 목표로 설계된 언어지만, 외부에 작업 수행을 맡기기도 한다.

native 키워드의 역할 및 특징

운영체제, 하드웨어 종속작업 처리

Java는 플랫폼 독립성을 가진 언어이지만, 운영 체제나 하드웨어에 종속적인 작업을 처리해야할 때 C/C++로 작성된 네이티브 코드를 호출해 운영 체제의 API나 하드웨어를 직접 제어해야 한다.

  • 예:
    • 파일 시스템 접근 (FileInputStream, FileOutputStream)
    • 네트워크 소켓 관리
    • 그래픽 처리를 위한 GUI 라이브러리 (AWT/Swing)
    • 시스템 시간 가져오기 (System.currentTimeMillis())
    • concurrent(동시성) 관련 코드

성능 최적화

Java는 일반적으로 JVM 위에서 동작하므로 C/C++로 작성된 네이티브 코드에 비해 약간의 오버헤드가 존재. 특히, 자바의 네이티브 메서드는 속도가 중요한 저수준 작업에서 성능을 극대화하기 위해 사용.

  • 예:
    • AtomicInteger와 같은 CAS (Compare-And-Swap) 연산은 C/C++로 구현된 네이티브 코드를 호출하여 고성능을 보장.
    • arraycopy()는 배열 복사를 네이티브 코드로 처리하여 높은 속도를 보장.

하드웨어 제어

Java는 추상화된 언어로, 하드웨어를 직접 제어하는 데 적합하지 않다. 하지만 네이티브 코드를 사용하면 Java 프로그램이 특정 하드웨어 장치를 제어하거나 센서 데이터를 읽을 수 있다.

  • 예:
    • 임베디드 시스템에서 Java와 네이티브 코드를 조합해 사용.
    • 특정 하드웨어 기능 호출.

멀티스레딩 및 동기화

Java는 스레드 관리와 동기화 관련 기능을 제공하지만, JVM의 성능을 극대화하거나 플랫폼 종속적인 동기화 메커니즘을 사용하기 위해 네이티브 메서드가 필요할 수 있다.

  • 예:
    • Thread.sleep()와 같은 메서드는 네이티브 코드를 통해 구현되어 운영 체제의 스레드 관리 기능을 호출.
    • AtomicIntegercompareAndSet() 같은 원자적 연산은 하드웨어 명령을 활용한 네이티브 코드로 구현.
반응형

멀티스레드 문제 예시 - 은행 계좌 잔액 갱신 문제

class BankAccount {
    private int balance = 100;

    public void withdraw(int amount) {
        if (balance >= amount) { // 조건 검사
            balance -= amount;  // 잔액 갱신
        }
    }
}

의도된 동작

  1. 스레드 A가 50을 출금.
  2. 스레드 B가 50을 출금.
  3. 결과적으로 잔액은 0이 되어야 함.

문제 상황

  • 스레드 A와 스레드 B가 동시에 withdraw(50) 메서드를 호출하면, 잔액 조건 검사와 갱신 작업이 중첩될 수 있음.
  • 이로 인해 두 스레드가 잔액 조건을 동시에 검사한 후, 동시에 출금을 진행.
  • 결과적으로, 잔액이 50으로 잘못 계산됨.

 

**정상적인 단일 스레드 실행**

-------
스레드 A 실행:
[검사] 잔액: 100 >= 50  -> True
[갱신] 잔액 -= 50       -> 잔액: 50

**멀티스레드에서의 충돌**
스레드 A 실행:                          스레드 B 실행:
[검사] 잔액: 100 >= 50  -> True        [검사] 잔액: 100 >= 50  -> True
[갱신] 잔액 -= 50       -> 잔액: 50    [갱신] 잔액 -= 50       -> 잔액: 50

결과: 두 스레드가 모두 출금을 완료했지만, 최종 잔액은 50으로 계산됨 (잘못된 결과).

멀티스레드 환경의 위험 요소

  1. Race Condition (경쟁 상태)
    • 여러 스레드가 동시에 동일한 자원에 접근하고 이를 수정할 때, 실행 순서에 따라 결과가 달라질 수 있음.
    • 예: 위의 은행 계좌 예제에서 스레드 간 동기화가 없을 경우 발생.
  2. Deadlock (교착 상태)
    • 두 스레드가 서로 자원을 기다리며 무한히 대기하는 상황.
    • 예: 스레드 A가 자원 1을 점유하고 자원 2를 기다리는 동안, 스레드 B는 자원 2를 점유하고 자원 1을 기다림.
  3. Data Corruption (데이터 손상)
    • 여러 스레드가 동일한 메모리 위치를 수정하면, 결과값이 예기치 않게 손상될 수 있음.
    • 예: 공유 데이터 구조의 상태가 불완전하거나 잘못된 상태로 유지됨.
  4. Thread Interleaving (스레드 중첩 실행)
    • 스레드가 실행 중에 문맥 교환(Context Switching)으로 인해 실행 흐름이 중첩됨.
    • 예: 스레드 A와 B가 교차 실행되며 연산이 중간 상태에서 중단됨.

해결 방법

1. 동기화 사용

  • synchronized 키워드: 공유 자원에 한 번에 하나의 스레드만 접근하도록 보장.
  • ReentrantLock: 더 세밀한 락 제어 가능.
public synchronized void withdraw(int amount) {
    if (balance >= amount) {
        balance -= amount;
    }
}

2. 원자적 연산 사용

  • Java의 AtomicInteger 또는 AtomicLong과 같은 클래스 사용.
  • 내부적으로 CAS(Compare-And-Swap)를 활용하여 동기화 문제를 해결.
import java.util.concurrent.atomic.AtomicInteger;

class BankAccount {
    private AtomicInteger balance = new AtomicInteger(100);

    public void withdraw(int amount) {
        balance.addAndGet(-amount); // 원자적 연산
    }
}

3. 동시성 제어 도구

  • CountDownLatch, Semaphore, CyclicBarrier 등 사용.
  • 특정 조건에서 스레드의 실행을 제어하여 동기화 문제를 방지.
반응형

+ Recent posts