Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Archives
Today
Total
관리 메뉴

밤빵's 개발일지

[TIL]20241122 synchronized 본문

개발Article

[TIL]20241122 synchronized

최밤빵 2024. 11. 22. 03:41

생각없이 보던 화면에 코드가 보여서 살펴보다가, 멀티스레드 환경에서 동기화 문제를 해결하기 위한 synchronized 키워드를 사용해 본 적은 없었기 때문에, 이 기회에 학습 겸 개발일지를 작성하기로 했다. 코드 예시를 통해 synchronized 키워드가 실제로 어떻게 사용되는지, 어떤 문제를 해결할 수 있는지, 그리고 이 키워드를 사용할 때 주의해야 할 점 까지 정리를 해보기로했다. 

▶ synchronized 키워드

멀티스레드 환경에서 여러 스레드가 하나의 공유 리소스에 동시에 접근하려 할 때, Race Condition과 같은 문제가 발생할 수 있다. synchronized 키워드는 이러한 문제를 방지하기 위해 사용되고, 공유 리소스에 대한 접근을 하나의 스레드로 제한한다.

특징

  • synchronized를 사용하면 동시에 하나의 스레드만 메서드나 코드 블록에 접근할 수 있다.
  • 데이터 무결성을 보장한다.
  • 객체 또는 클래스 수준에서 동기화를 적용할 수 있다.

▶ 동기화가 필요한 상황: Race Condition

Race Condition은 여러 스레드가 공유 리소스를 동시에 수정하려 할 때 발생한다. 예를 들어, 두 스레드가 파일에 데이터를 쓰는 작업을 수행한다면, 데이터가 손실되거나 파일이 손상될 가능성이 있다. 이러한 상황에서 synchronized를 사용하면 문제를 방지할 수 있다.


▶코드 예시

synchronized 키워드가 적용된 코드로, 영상에서 나온 코드는 메서드가 비어있어서 대충 흐름에 맞게 채워 넣었다. 이 코드는 파일에 사용자 리포트를 추가하거나, 파일에서 사용자 리포트 리스트를 읽어오는 기능을 제공한다.

synchronized public void addReportEntry(UserReport urp) {
    File idxFile = new File(userroot, idxFile);
    FileWriter fw = null;

    try {
        fw = new FileWriter(idxFile, true); // 파일에 append 모드로 열기
        fw.write(urp.encode() + "\n");      // UserReport를 문자열로 변환하여 파일에 쓰기
        fw.flush();                         // 파일 내용을 즉시 저장
    } catch (IOException e) {
        logger.log(Level.SEVERE, "Error writing to file", e);
    } finally {
        if (fw != null) {
            try {
                fw.close(); // 파일 닫기
            } catch (IOException e) {
                logger.log(Level.WARNING, "Error closing file", e);
            }
        }
    }
}

→ addReportEntry를 호출한 여러 스레드가 동일한 파일에 접근하는 상황에서, 한 번에 한 스레드만 파일을 수정하도록 보장한다.

synchronized public List<UserReport> list() {
    List<UserReport> replist = new java.util.ArrayList<UserReport>();
    File idxFile = new File(userroot, idxFile);

    if (idxFile.exists()) {
        BufferedReader br = null;

        try {
            br = new BufferedReader(new FileReader(idxFile));
            String line = null;

            while ((line = br.readLine()) != null) {
                UserReport rep = UserReport.decodeLine(line);
                if (rep != null) {
                    replist.add(rep);
                    if (rep.getId() > this.maxId) maxId = rep.getId();
                }
            }
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception while reading file", ex);
        } finally {
            if (br != null) {
                try {
                    br.close(); // BufferedReader 닫기
                } catch (IOException e) {
                    logger.log(Level.WARNING, "Error closing BufferedReader", e);
                }
            }
        }
    }

    return replist;
}

→ 파일에서 사용자 데이터를 읽어 리스트로 반환한다. 파일이 존재하지 않을 경우 작업을 건너뛰고, synchronized를 통해 여러 스레드가 동시에 파일을 읽는 상황에서도 안정성을 보장한다.

 

▶ synchronized의 작동 방식

메서드 수준 동기화: 위 코드는 synchronized를 메서드 수준에 적용했다. 해당 객체(this)를 락으로 사용한다.

락(Lock) 개념: 한 스레드가 synchronized 메서드에 접근하면 다른 스레드는 락이 해제될 때까지 대기한다.

객체 수준 동기화: 메서드의 synchronized는 해당 객체의 모든 synchronized 메서드에 적용된다.


▶ synchronized의 장단점

장점

  • 구현이 간단하다. 한 키워드만으로 동기화 문제를 해결할 수 있다.
  • 데이터 무결성을 보장한다. 여러 스레드가 동시에 공유 리소스에 접근하여 발생할 수 있는 문제를 방지한다.

단점

  • 병목 현상: 동기화를 잘못 사용하면 성능 저하가 발생할 수 있다. 특히 대량의 스레드가 경쟁하는 상황에서는 효율성이 떨어질 수 있다.
  • 유연성 부족: 세밀한 제어가 필요한 경우 synchronized는 한계가 있다.

▶ ReentrantLock

ReentrantLock은 synchronized보다 더 세밀하게 동기화를 제어할 수 있는 고급 메커니즘이다.

ReentrantLock의 장점

  • 락의 획득과 해제를 명시적으로 제어할 수 있다.
  • 타임아웃을 설정해 스레드가 무한 대기하지 않도록 할 수 있다.
  • 락 공정성(Fairness) 설정이 가능하다.
private final ReentrantLock lock = new ReentrantLock();

public void addReportEntry(UserReport urp) {
    lock.lock();
    try {
        // 파일 쓰기 로직
    } finally {
        lock.unlock();
    }
}

멀티스레드 환경에서 공유 리소스의 동기화는 매우 중요한 부분이고, synchronized 키워드는 간단한 동기화 문제를 해결하는 데 유용하다. 그러나 성능 이슈나 더 세밀한 제어가 필요한 경우에는 ReentrantLock과 같은 대안을 고려해야 한다.

 

+추가) 개발일지 소재가 된 코드 🤭