반응형

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

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 등 사용.
  • 특정 조건에서 스레드의 실행을 제어하여 동기화 문제를 방지.
반응형
반응형

AtomicInteger.class

Java에서 사용하는 클래스로 멀티 스레드 환경에서 안전하게 사용되기 위한 원자성을 보장받는 자료형이다

클래스의 주요 기능 및 특징

  • 원자적 연산
    • 다른 스레드가 해당 연산의 중간 상태를 볼 수 없음
  • 스레드 안전
    • 여러 스레드에서 동시에 접근하더라도 데이터 경합 없이 안전하게 값을 읽고 쓸 수 있음
  • 락 없이 동작
    • 기존의 동기화 방식(synchronized 키워드)은 락을 사용하지만, 아토믹 연산은 CPU가 제공하는 하드웨어 수준의 락-프리(Lock-Free) 매커니즘을 사용
  • CAS 알고리즘
    내부적으로 CAS(Compare-And-Swap) 알고리즘을 사용하여 값을 업데이트
    • 특정 메모리 위치 값이 예상값과 같은지 확인한 후, 같다면 새 값으로 교체
    • 이 과정에서 충돌을 감지

주요 메서드

  • get()
  • set(int newValue)
  • getAndIncrement()
    • 현재 값 반환 후 1 증가
  • incrementAndGet()
    • 값을 1 증가시키고 증가된 값 반환
  • compareAndSet(int expect, int update)
    • 현재 값이 expect와 같으면 update 값으로 변경 (CAS 연산)

사용 고려 시점

  • 동시성 프로그래밍에서 데이터를 안전하게 업데이트해야 하는 경우
    • 멀티 스레드 환경에서 카운터 증가
    • 통계 집계
  • 락 기반 동기화보다 가벼운 대안
  • 성능이 중요한 환경에서 데이터 무결성을 유지해야 할 때

❓CAS (Compare-And-Swap)

CAS는 값의 업데이트가 특정 조건에서만 이루어지도록 보장하는 하드웨어 지원 원자적 연산
따라서 자바에만 있는 것이 아니라, C 계열의 언어에서도 std::atmoic 클래스와 같이 사용되고 있음

CAS 작동 방식

  • 특정 메모리 위치에 대해, 세 값을 비교 및 업데이트
    • 현재 값 (current): 현재 메모리에 저장된 값
    • 기대 값 (expected): 우리가 예상하는 값
    • 새 값 (new): 우리가 저장하려는 값
  • CAS 연산의 과정
    • 메모리의 현재 값이 기대 값(expected)과 동일하면, 새 값으로 업데이트
    • 메모리의 현재 값이 기대 값과 다르면 실패를 반환

CAS 장점

  • 스케일링 가능성: 여러 스레드가 동시에 접근해도 성능 저하가 적다.
  • 스레드가 블로킹되지 않으므로 데드락 위험성이 없다

CAS 단점

  • Busy-waiting:
    • 실패할 경우 재시도하는 루프를 사용하므로 CPU 자원을 소비할 수 있다.
    • 특히 충돌이 빈번하면 성능 저하가 발생할 수 있다.
  • ABA 문제:
    • 메모리 값이 A -> B -> A로 변경된 경우, CAS는 값이 바뀌지 않았다고 판단할 수 있다.
    • 이 문제는 태그(tag) 또는 버전 관리를 통해 해결

코드 예시 - 과정만 참고

public final int incrementAndGet() {
    for (;;) {
        int current = get(); // 현재 값 읽기
        int next = current + 1; // 새로운 값 계산
        if (compareAndSet(current, next)) // CAS 연산
            return next; // 성공 시 반환
    }
}

CPU의 원자적 명령어

CPU는 CAS 연산을 지원하기 위해 특정 명령어(기계어)를 제공

  • x86 아키텍처 - CMPXCHG(Compare and Exchange)
  • ARM 아키텍처 - LDREX/STREX

메모리 값을 읽고 수정하는 작업을 한 번에 처리하므로, synchronized 키워드 없이 안전하게 업데이트


반응형

'호기심 천국 > Java' 카테고리의 다른 글

멀티스레드 문제 상황 예시 1  (1) 2025.01.20
StringBuilder vs String 문자열합치기  (0) 2023.09.19
반응형

도메인은 특정한 주제나 분야에 대한 지식이나 활동을 가리키는 일을 뜻하는데,
특정 도메인을 전문적으로 사용하되, 이해하기 쉽고, 사용하기 쉬울 수 있도록 개발된 언어

장점

  • 런타임 오버헤드를 발생시키지 않음
  • 외부 DSL 사용 시 플랫폼에 자유로워짐

단점

  • 개발 비용이 비쌈

⇒ 우리가 클린코드를 지향하는 이유와 DSL이 나온 이유가 같다고 생각함

DSL의 예시

  • SQL (데이터베이스 질의 언어)
  • Gradle (빌드 자동화 도구)
  • Query DSL
  • 자바의 Stream API

참고

https://www.jetbrains.com/ko-kr/mps/concepts/domain-specific-languages/

 

도메인 특화 언어(DSL)에 관한 설명 | JetBrains MPS

 

www.jetbrains.com


모던 자바 인 액션

반응형
반응형

스프링 시큐리티 공식문서를 참고해서 정리해본 시큐리티 관련 내용

인증(비밀번호)

Password Storage :: Spring Security

스프링 시큐리티에 있는 passwordEncoder인터페이스는 비밀번호의 단방향 변환의 기능을 갖고있다.

유저데이터의 비밀번호 보안 역사

제일 처음 유저데이터의 정보는 평문으로 저장했었다. why? : 유저데이터는 DB에 들어가있으니 DB 어드민이 털리지 않는 이상 문제 없을거라 판단.

이후 악의적인 사용자는 SQL Injection같은 공격을 통해 사용자 이름과 비밀번호가 포함된 데이터를 얻을 방법을 찾아냈고, 이러한 방식으로 데이터의 보안이 뚫리기 시작하자 새로운 암호화 방식인 단방향해시 SHA-256을 통해 정보를 해시화하고 해당 해시를 저장하도록 권장되었다.

이러한 방식은 사용자가 인증을 시도하면 해시된 비밀번호가 입력한 비밀번호의 해시와 비교해서 일치한다면 로그인이 되는데, 이러한 암호화를 뚫기 위해 Rainbow Tables라는 조회 테이블이 생겼고 해당 테이블은 비밀번호의 입력값을 미리 해시화해서 테이블에 넣어두고 역인코딩으로 값을 받아온다

때문에 해당 방식으로도 값을 뚫리게 되고 이를 막기위해 개발자는 소금을 치기 시작한다(특정하지 않은 랜덤한 문자열을 해시문에 포함시킴) 이러한 형식으로 Rainbow Tables 조회테이블에 당하지 않는게 현재 상황이다.

이러한 보안상의 이슈를 스프링 시큐리티에서 자체적으로 지원을 해주는데, 추가로 생각해야 하는 것이 비밀번호 검증을 할 때 의도적으로 리소스(CPU, 메모리, 등등)를 많이 사용하는것이다.

비밀번호를 확인하는데 1초의 지연시간을 주는 이유는 공격자가 암호를 무차별확인을 하면서 해독할 수 있는 시간을 주지 않는것이 주요 목표이다.

때문에 스프링 시큐리티 인증 설정을 할 때 해당 부분을 잘 신경써서 같이 구현해야 할 것이다.

또 위와같이 데이터를 찾는데 시간을 주지않기 위해 장기자격증명이 아닌 단기자격증명으로 지속체크를 하는 것이 좋을 것이다.

 

장기자격증명 → 사용자 이름 및 비밀번호

단기자격증명 → 세션, Oauth토큰

User.UserBuilder users = User.withDefaultPasswordEncoder();

or 

UserBuilder users = User.withDefaultPasswordEncoder();
UserDetails user = users
  .username("user")
  .password("password")
  .roles("USER")
  .build();
UserDetails admin = users
  .username("admin")
  .password("password")
  .roles("USER","ADMIN")
  .build();

기본 Deprecated되어있는데 이유는 메모리와 컴파일된 소스코드에 비밀번호가 노출되어있기 때문에 해당 비밀번호를 외부에서 해시를 해야 안전해짐

때문에 사용x

 

위의 사진은 스프링 시큐리티 클래스의 User클래스 내부 코드중 일부인데 이런식으로 생성자를 통해서도 가입이 가능한듯?

패스워드 인코더는 자바 자체에서 지원하는데,

  • bcrypt
  • ldap
  • MD4
  • MD5
  • noop
  • pbkdf2
  • scrypt
  • SHA-1
  • SHA-256
  • sha256

그냥 이런게 있구나하고 인지하면될듯

주요한 것은 스프링시큐리티에 있는

  • BCryptPasswordEncoder
  • Argon2PasswordEncoder
  • Pbkdf2PasswordEncoder
  • SCryptPasswordEncoder

이 네가지 인코더인데 상황이나 여건에 따라서 네가지중 하나를 사용하면서 비밀번호를 확인하는 시간을 약 1초가 걸리도록 지연하게 하는것이 안전하다

이유는 레인보우테이블 공격을 통해 무차별 인코딩확인을 막기 위해.

PasswordEncoder 또한 지원을 하긴 하는데 현재는 안전하지 않다는 것이 검증되었다. 지원하는 이유는 이전 버전에서 만들어진 코드는 해당 인코더를 사용했었기 때문에

만들어진 비밀번호를 변경하는 방법은

http
    .passwordManagement(Customizer.withDefaults())
http
    .passwordManagement((management) -> management
        .changePasswordPage("/update-password")
    )

위의 방식으로 사용하면 될 것 같음

 

추가로

약관에 동의하더라도 개인정보는 평문으로 저장하면 안된다

이 때문에 스프링 시큐리티의 도움을 반드시 받는것이 중요할 것 같다.

혹시나 서비스할 때 항상 잊지않고 생각해야하는 부분

 

비밀번호 해시 관련 지식참조 사이트

https://starplatina.tistory.com/entry/비밀번호-해시에-소금치기-바르게-쓰기

반응형
반응형

빌더는 Capacity와 count를 갖고있다. 빌더 안에서 문자열을 합쳐도 StringBuilder의 capacity를 넘지 않는다면 new 연산을 하지 않음

이유는 아래 설명을 통해 풀어가보겠다

StringBuilder : 기존의 capacity일 때는 새로운 메모리에 문자열을 추가하는 것이 아닌 기존의 메모리에 담기 때문에.

String은 불변하고, 불변성의 특징을 갖기 때문에 문자열을 추가할 때는 두개를 서로 붙여서 새로운 메모리를 할당하는 new 연산이 들어감

추가로 StringBuilder와 기능은 동일한 StringBuffer를 사용한다면 성능이 약간 낮아지는 대신에 Thread-Safe하다는 것까지 체크하면 좋을 것 같다.

문자열 합칠 때 비교

  • String : 무조건 new String(a + b);
  • StringBuilder : a.length+b.length < capacity ? StringBuilder(a + b) : new StringBuilder(a + b);

String클래스의 배열 final byte[]

 

StringBuilder는 추상클래스AbstractStringBuilder를 상속받고, 해당 클래스에서 char[]배열은 final선언이 안되어있음

 

때문에 동작은 아래와 같음

 

결론 : 문자열을 더할 때는 String 대신 StringBuilder나 StringBuffer를 사용하자.

참고 사이트 : https://cjh5414.github.io/why-StringBuffer-and-StringBuilder-are-better-than-String/

https://readystory.tistory.com/141

반응형

'호기심 천국 > Java' 카테고리의 다른 글

멀티스레드 문제 상황 예시 1  (1) 2025.01.20
AtomicInteger (멀티스레드 안전) 겉 핥기  (0) 2025.01.19
반응형

이번 프로젝트에서 리더보드를 구현하는 부분을 맡게 되었다.

리더보드에서는 해당 유저의 엔티티를 통해 하나의 값을 기준으로 내림차순으로 나열하는 방식이였는데, 때문에 JPA를 통해서 findAll로 받고 java에서 Arrays.sort형식으로 구현하려 했었는데 막상 구현하려고 하니 DB에서 데이터를 findAll로 받아서 Java로 구현한다면

"자바를 통해 다시한번 해당 데이터를 처리해야하기에 메모리에 손해가 있지 않을까?"

라는 의문을 시작으로 그렇다면 DB를 사용해서 정렬을 활용하면 더 빠를까?라는 생각을 통해 기본적으로 pk값인 id는 인덱스가 걸려있어서 속도걱정을 안해도 되겠지만 다른 특정한 값에 인덱스를 넣지 않고 orderByDesc를 해야하는 상황이였기에 이게 빠른지 저게 빠른지 모르는 상태였다.

현재는 프로젝트가 대규모 데이터를 갖고있지는 않기에 얼마나 성능적인 면에서 차이가 있을지는 모르겠어서 내가 할 수 있는 최대한의 방법인 검색을 통해 데이터를 수집해 보았고, 정리를 해 보았다.

  • 해당 데이터가 DB측에서 정리가 가능게 되어있는 가공된 데이터들 orderByDesc, Asc 등등 예외사항이 나올 수 없는 경우에 일반적으로 DB에서 처리하는 것이 빠르고 안정적
  • 위에서 언급했던 자바에서 해당 데이터를 한번 더 처리해야하기에 O(2n)의 시간복잡도를 가지게 될 것이라 생각한다.

 

GPT를 통한 답변도 비슷하게 나왔다.

JPA로 정렬하고 가져오는 것과 JPA의 findAll을 통해서 리스트를 가져온 다음 Java로 메서드를 작성해서 정렬하는것 어느게 더 빨라?
JPA
(Java Persistence API
)를 이용한 데이터베이스 쿼리와 Java의 메서드를 이용한 데이터 정렬은 각각 장단점이 있습니다
다음은 주요 차이점과 고려사항입니다

JPA로 정렬하고 가져오는 방법:
성능 : 데이터베이스에서 최적화된 정렬 알고리즘을 이용하기 때문에 대체로 빠릅니다
네트워크 부하 : 필요한 데이터만 가져올 수 있어서 네트워크 트래픽이 줄어듭니다
메모리 사용량 : 필요한 데이터만 메모리에 로드되므로 효율적입니다

Java로 메서드를 작성해서 정렬하는 방법
성능 : Java에서 처리하는 정렬은 데이터베이스의 최적화된 알고리즘보다 일반적으로 느립니다
네트워크 부하 : 모든 데이터를 가져와야 하므로 네트워크 트래픽이 증가할 수 있습니다
메모리 사용량 : 모든 데이터를 메모리에 로드해야 하므로 메모리 사용량이 늘어납니다

이러한 차이점을 고려하면 일반적으로는 JPA를 이용해서 데이터베이스에서 정렬하고 가져오는 것이 더 효율적입니다
그러나 상황에 따라 다를 수 있으므로
성능 테스트를 통해 가장 적합한 방법을 선택하는 것이 좋습니다

결론은 데이터를 추가로 가공할 것이 아니면 자바에서 findAll로 받아올 필요가 없을 것이라 정리가 되었다.

다음에 정렬할 일이 있다면 더 신중하고 정확하게 어느걸 고를지 판단할 수 있을듯 한 느낌😊

반응형

'호기심 천국 > Spring' 카테고리의 다른 글

스프링 시큐리티 1 (인증)  (0) 2023.09.26
스프링부트 final로 클래스 생성자주입  (0) 2023.07.27
반응형

 

스프링 공부를 하면서 final키워드를 만났을 때 평소와 다른 어색한 사용방법 때문에 의문점이 생겼다.

final인데 왜 주입이 되는거지?
@Controller
public class MemberController{
		private final MemberService memberService;

		@Autowired
		public MemberController(MemberService memberService){
				this.memberService = memberService;
		}
		
}

final 선언 후 첫 값 초기화는 가능해서 그런건가? 라는 의문이 들었다.

이유는, 평소 final키워드 선언할 때는 항상 선언과 동시에 값을 초기화하기 때문이다.

이러한 부분이 모호하다 생각해서 final과 함께 서비스를 공부해보았다.

 

final


 

final 키워드는 변경이 더이상 필요없을 때 선언해줄 수 있다.

이번에 스프링부트를 사용하며 의문이 들었기에 위의 코드를 기준으로 스프링부트에서 생성자주입을 할때,

우리는 해당 객체의 메서드의 변경이 더이상 필요없다고 인지를 하고 있을 것이다.

때문에 Service객체에서 사용되는 모든 메서드들은 어디에서 사용이 되던 동일한 역할과 책임이 있다.

그러한 이유로 다른 컨트롤러든 서비스에서든 해당 Service에서 나올 기댓값은 동일하기에 클래스 추가생성을 통해 자원을 낭비할 필요가 없으니 final 선언을 하는 것이고, 이것이 싱글톤형태로 사용되고있는 이유다.

여러 클래스에서 해당 Service클래스를 여러번 선언하고 생성자주입을 한다면 싱글톤으로 돌아갈 수 없다고 생각될 수 있다.

하지만 이 부분은 스프링 빈의 특성을 통해서 해결이 가능하다 스프링 빈은 기본적으로 싱글톤 스코프로 관리되기 때문에 한번 해당 클래스가 스프링 빈으로 등록이 된다면 이후 동일한 인스턴스가 계속 동작한다

반응형

'호기심 천국 > Spring' 카테고리의 다른 글

스프링 시큐리티 1 (인증)  (0) 2023.09.26
데이터베이스 정렬 vs 자바 정렬  (0) 2023.09.12

+ Recent posts