Skip to content

이지표 6주차 학습일지

이지표 edited this page Aug 8, 2024 · 1 revision

Filter

  • Filter는 J2EE 스펙 기능으로 디스패처 서블릿에 요청이 전달되기 전/후 url패턴에 맞는 모든 요청에 대해 부가 작업을 처리할 수 있는 기능을 제공
  • init : 필터 객체를 초기화하고, 서비스에 추가하기 위한 메서드이다.
  • doFilter: url-pattern에 맞는 모든 HTTP요청이 디스패처 서블릿으로 전달되기 전에 웹 컨테이너에 의해 실행되는 메서드이다.
  • destroy: 필터 객체를 서비스에서 제거하고 사용하는 자원을 반환하기 위하 메서드이다.

Interceptor

  • Spring에서 제공하는 기술로, 디스패처 서블릿이 호출되기 전과 후에 요청, 응답을 참조하거나 가공할 수 있는 기능을 제공한다.
  • preHandle: 컨트롤러가 호출되기 전에 실행된다.
  • postHandle: 컨트롤러를 호출한 후 실행된다.
  • afterCompletion: 모든 작업이 완료된 후 실행된다.

MySQL PreparedStatement

  • MySQL prepared statement는 데이터베이스 서버에서 쿼리를 미리 준비하고 클라이언트 서버에서 파라미터를 따로 전달하는 방식이다.
  • PREPARE 문으로 쿼리 틀을 만들고, EXECUTE로 실행한다.
  • 파라미터는 ?로 표시하며 USING 절에서 값을 지정한다.
  • SQL 인젝션 방지, 성능 향상, 동적 쿼리 구성이 가능하다. 사용 후엔 DEALLOCATE PREPARE로 해제한다.
  • 테이블명이나 컬럼명을 동적으로 쓸 땐 보안에 주의해야 한다.
  • 세션 종료 시 자동으로 해제된다.

Soft Deletion

  • Soft Deletion은 데이터를 실제로 삭제하지 않고 논리적으로만 삭제 처리하는 기법이다.
  • 주로 'is_deleted' 또는 'deleted_at' 같은 필드를 추가하여 삭제 여부를 표시한다.
  • 데이터 복구가 용이하고, 감사 및 로깅에 유용하며, 참조 무결성을 유지할 수 있다.
  • 데이터베이스 크기 증가, 쿼리 성능 저하, 애플리케이션 로직 복잡화 등의 단점이 있다.
  • 불리언 플래그나 타임스탬프를 사용하여 구현할 수 있다.
  • 주기적인 데이터 정리 프로세스 구현이 권장된다.

메모리 모델(Memory Model)이란?

메모리 모델은 프로그램의 실행 방식과 메모리 접근 규칙을 정의합니다. Java Memory Model(이하 JMM)은 멀티 스레드 환경에서 변수의 값을 어떻게 읽고 쓰는지, 그리고 동기화를 어떻게 수행하는지를 규정합니다.

JMM을 이해하기 위해서는 두 가지 중요한 개념을 알아야 합니다:

  1. 명령어 재정렬 (Instruction Reorder)
  2. 메모리 가시성 (Memory Visibility)

1. 명령어 재정렬 (Instruction Reorder)

예제를 먼저 살펴보겠습니다.

public class InstructionReorderTest {

    private boolean flag;
    private int x;
    private int result;

    public void actor1() {
        if (this.flag) {
            result = x;
        } else {
            result = -1;
        }
    }

    public void actor2() {
        x = 5;
        flag = true;
    }
}

두 스레드가 각각 actor1()actor2()를 동시에 실행한다고 가정해 봅시다. result에는 어떤 값이 존재할까요?

x에는 5를 할당하고, 이후 flagtrue를 할당하기에 actor1()이 먼저 실행되면 5가, actor2()가 먼저 실행되면 result에는 -1이 들어있을까요?

OpenJDK에서는 Java의 동시성 테스트를 지원해 주는 JCStress라는 툴을 제공하고 있습니다. 이 테스트 툴을 활용해서 결과를 확인해 봅시다.

@Outcome(id = {"0"}, expect = Expect.ACCEPTABLE_INTERESTING,
				desc = "예상하지 못한 결과")
@Outcome(id = {"-1", "5"}, expect = Expect.ACCEPTABLE,
				desc = "예상한 결과")
@JCStressTest(Mode.Continuous)
@State
public class InstructionReorderTest {

    private boolean flag;
    private int x;

    @Actor // 동시에 실행
    public void actor1(I_Result result) {
        if (this.flag) {
            result.r1 = this.x;
        } else {
            result.r1 = -1;
        }
    }

    @Actor // 동시에 실행
    public void actor2(I_Result result) {
        this.x = 5;
        this.flag = true;
    }
}

테스트 결과

실행 환경 Intel Core i9, RAM 32GB, Mac OS X, JDK 17(Corretto 17.0.1)

RESULT SAMPLES FREQ EXPECT DESCRIPTION
5 1,212,781,984 67.98% Acceptable 예상한 결과
-1 571,150,571 32.02% Acceptable 예상한 결과
0 47,477 <0.01% Interesting 예상하지 못한 결과

일반적으로 우리는 5 또는 -1만 가능할 것이라고 예상합니다. 하지만 실제로는 0이 나올 수도 있습니다. 왜 이런 현상이 발생할까요? 이는 명령어 재정렬(Instruction Reorder) 때문입니다.

// 재정렬된 actor2() 메서드
public void actor2() {
    flag = true;  // 순서가 바뀜
    x = 5;
}

성능 최적화를 위해 컴파일러, JVM, CPU등 명령어의 실행 순서를 변경할 수 있는데요. 이를 명령어 재 정렬이라고 합니다. 더 자세한 내용을 알고 싶으신 분들은 Synchronization and the Java Memory Model글을 보시는 것을 추천드립니다.

2. 메모리 가시성 (Memory Visibility)

또 다른 예시를 살펴보겠습니다.

@Outcome(id = "TERMINATED", expect = Expect.ACCEPTABLE_INTERESTING, 
				desc = "정상적으로 종료")
@Outcome(id = "STALE", expect = Expect.ACCEPTABLE,
				desc = "종료되지 않음")
@JCStressTest(Mode.Termination)
public class VisibilityTest {

    private boolean flag = true;

    @Actor // 실행
    public void actor() {
        while (flag) {
						// 어떤일을 한다..
        }
    }

    @Signal // 종료 신호
    public void signal() {
        this.flag = false;
    }
}

한 스레드는 while문을 실행시키고, 다른 스레드가 종료시키기 위해 flagfalse로 변경하면 무한 루프가 종료될까요? 결과를 확인해 봅시다.

테스트 결과

실행 환경 Intel Core i9, RAM 32GB, Mac OS X, JDK 17(Corretto 17.0.1)

RESULT SAMPLES FREQ EXPECT DESCRIPTION
TERMINATED 7,199 99.92% Acceptable 정상적으로 종료
STALE 6 0.08% Interesting 종료되지 않음

결과를 확인해 보니 낮은 확률이지만 6건 정도는 while문에서 탈출하지 못하는 현상이 발생하네요! 이 현상은 왜 그럴까요? 이는 메모리 가시성과 관련이 있습니다.

출처: https://jenkov.com/tutorials/java-concurrency/false-sharing.html

각 스레드는 빠른 연산을 위해 메인 메모리에 바로 flush 하지 않고, 연산한 값들을 로컬 캐시에 저장하게 됩니다. 그 결과 flag가 변경된 결과는 메인 메모리에 바로 반영이 되지 않을 수 있고, while문을 실행하는 스레드도 메인 메모리에 있는 값을 안 가져올 수 있습니다.

이러한 상황들이 발생하면서 whlie문에서 탈출하지 못하고, 프로그램이 종료되지 않는 사례가 발생합니다.

volatile 키워드

이러한 동시성 문제를 해결해 주는 keyword가 있습니다. 바로 volatile입니다. volatile은 로컬 캐시에 저장하지 않고, 바로 메인 메모리에 flush 하게 됩니다. 하지만 메인 메모리에만 flush 하는 것으로는 모든 것을 해결할 수 없습니다. 바로 명령어 재정렬 현상이 발생하기 때문입니다.

다른 스레드가 메인 메모리에 있는 값들을 가져오더라도, volatile 이전에 연산된 값(이하 Happens Before)들이 보장되지 않으면 올바른 연산을 보장하지 못하기 때문입니다.

예전 JDK1.4 이전에서는 Happens Before를 보장하지 않았습니다. 많은 자바 개발자가 고통받은 후 JDK1.5부터 새로운 JMM을 도입하게 되어, 연산의 순서를 보장할 수 있었습니다. 이는 JSR-133에서 자세히 설명하고 있습니다.

public class VolatileKeyword() {

	int a, int b;
	volatile int c;

	public void write() {
		a = 1;
		b = 2;
		c = 3;
	}
		
	public void read() {
		if (c == 3) { // volatile을 읽어들인 이후
			System.out.println(b); // Happens Before 보장
			System.out.println(a);
		}
    }
}

위 예시에서도 volatilec를 읽어 들이면, Happens Before를 보장하여 b2a3을 출력하게 됩니다.

명령어 재정렬과 메모리 가시성을 예시로 든 코드를 volatile을 붙인 후 다시 한번 결과를 확인해 볼까요?

@Outcome(id = {"0"}, expect = Expect.ACCEPTABLE_INTERESTING,
				desc = "예상하지 못한 결과")
@Outcome(id = {"-1", "5"}, expect = Expect.ACCEPTABLE,
				desc = "예상한 결과")
@JCStressTest(Mode.Continuous)
@State
public class InstructionReorderTest {

    private volatile boolean flag;
    private int x;

    @Actor // 동시에 실행
    public void actor1(I_Result result) {
        if (this.flag) {
            result.r1 = this.x;
        } else {
            result.r1 = -1;
        }
    }

    @Actor // 동시에 실행
    public void actor2(I_Result result) {
        this.x = 5;
        this.flag = true;
    }
}
RESULT SAMPLES FREQ EXPECT DESCRIPTION
5 809,314,396 23.44% Acceptable 예상한 결과
-1 2,643,089,316 76.56% Acceptable 예상한 결과
0 0 0.00% Interesting 예상하지 못한 결과
@Outcome(id = "TERMINATED", expect = Expect.ACCEPTABLE_INTERESTING, 
				desc = "정상적으로 종료")
@Outcome(id = "STALE", expect = Expect.ACCEPTABLE,
				desc = "종료되지 않음")
@JCStressTest(Mode.Termination)
public class VisibilityTest {

    private volatile boolean flag = true;

    @Actor // 실행
    public void actor() {
        while (flag) {
						// 어떤일을 한다..
        }
    }

    @Signal // 종료 신호
    public void signal() {
        this.flag = false;
    }
}
RESULT SAMPLES FREQ EXPECT DESCRIPTION
TERMINATED 28,293 100.00% Acceptable 정상적으로 종료
STALE 0 0.00% Interesting 종료되지 않음

결과를 확인해 보니 모두 예상한 결과가 나왔습니다.

volatile만?

Java는 volatile뿐만 아니라 다양한 동기화 키워드와 패키지를 제공하는데요. synchronized, Locks, java.util.concurrent, final, Thread 연산(join, start)도 volatile과 같은 규칙이 적용됩니다.

결론

이런 의문이 들 수 있습니다. “Java 개발하면서 이런것 까지 알아야 할까요? “, “그냥 Java에서 제공해 주는 concurrent 패키지를 쓰면 되지 않나요?”, “정말 낮은 확률인데…?” 등등…

만약 서비스를 운영하다가 동시성 문제가 발생한다면, 원인을 찾기 힘들 수 있습니다. 재현하기도 무척 힘들 수 있습니다. 하지만 이런 지식을 안다면, 원인을 찾기도 그나마 쉽고 동시성 문제가 발생하지 않도록 코드를 작성할 수 있지 않을까요?

JMM을 공부하면서 컴파일러, JVM, CPU가 최적화하는 방식에 대해 “왜 이렇게 프로그래머에게 고난을 주었나”라는 생각이 잠깐 든 적이 있었습니다. 하지만, 컴파일러, JVM, CPU는 죄가 없습니다. 당연히 최적화를 진행하여 최고의 성능을 발휘해야 하는 몫을 가지고 있습니다. 이러한 특성을 고려하고, 안정적인 코드를 작성하는 것은 프로그래머의 몫입니다.

지금까지 정말 간단하게 JMM에 대해 알아보았습니다. JMM에는 이뿐만 아니라 다양한 규칙이 있습니다. 더 궁금하신 분들은 출처에 적은 사이트를 한 번씩 방문하는 것을 추천해 드립니다.

마지막으로 저와 같이 고민해 주고, 토의해 주신 우아한 테크 캠프 캠퍼분들에게 감사합니다!

출처

https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html

https://shipilev.net/blog/2014/jmm-pragmatics/

https://jenkov.com/tutorials/java-concurrency/java-memory-model.html

https://youtu.be/Z4hMFBvCDV4?si=MJQFDHIYxzLN8Kw4

https://youtu.be/qADk_tj4wY8?si=ZGysCOy_8M6pw4CI

👼 개인 활동을 기록합시다.

개인 활동 페이지

🧑‍🧑‍🧒‍🧒 그룹 활동을 기록합시다.

그룹 활동 페이지

🎤 미니 세미나

미니 세미나

🤔 기술 블로그 활동

기술 블로그 활동

📚 도서를 추천해주세요

추천 도서 목록

🎸 기타

기타 유용한 학습 링크

Clone this wiki locally