-
Notifications
You must be signed in to change notification settings - Fork 1
week 3 yongseon
위 그림과 같이 JDK7까지 사용된 Permanent General(PermGen) Space가 Oracle의 주도하에 제거된 것이 큰 특징 중 하나입니다.
Metaspace : 새로운 메모리 저장소의 탄생
JDK8 HotSpot JVM의 경우, 재사용성을 위한 객체에 대한 metadata를 Metaspace라고 불리우는 Native Memory에 저장하게 되어 OS가 자동으로 크기를 조절하기 때문에 기존과 비교해 큰 메모리 영역을 사용할 수 있게 되었습니다. 그로 인해 Perm 영역 크기로 인한 java.lang.OutOfMemoryError : PermGen Space를 더 보기 힘들어 졌고, 더 이상 tunning과 monitoring을 통해서 이러한 memory space를 조절할 필요가 없다는 것입니다. 그렇지만, 이러한 변화는 기본적으로 안보이게 설정이 되기 때문에 우리는 이 새로운 meta memory에 대한 족적을 여전히 살펴볼 필요성은 존재하게 됩니다.
요약
PermGen space situation
- memory space는 완벽하게 제거됩니다.
- PermSize와 MaxPermSize JVM argument는 무시되며, 시작될때 경고를 표시합니다.
Metaspace memory allocation model
- class의 metadata는 거의 대부분 native memory에 저장됩니다.
- class의 metadata를 사용하기 위한 klasses들은 모두 제거됩니다.
Metaspace capacity
- 기본적으로 metaspace는 native memory를 사용하기 때문에, OS에서 사용 가능한 memory 영역 모두를 사용 가능합니다.
- 새로운 Flag(MaxMetaSpaceSize)가 가능합니다. 이는 class metadata를 저장하기 위한 최대한의 memory size를 정하는 것이 가능합니다. 만약에 Flag를 설정하지 않는다면, Metaspace는 application의 동작에 따라 가변적으로 동작하게 됩니다.
Metaspace garbage collection
- dead class와 classloader의 GC는 MaxMetaspaceSize에 도달하면 발생합니다.
- Metaspace의 tuning과 monitoring은 GC의 횟수와 지역을 제한하기 위해 필요한 작업입니다. 과도한 Metaspace GC는 classloader의 memory leak 또는 적합하지 않은 Application의 memory size를 초래합니다.
Java heap space impact
- 여러 다양한 데이터들은 Java Heap space로 저장 영역이 이동되었습니다. 결국 기존 JDK7에서 JDK8로 업그레이드를 하는 경우, Java Heap memory를 증가시킬 필요가 있습니다.
Metaspace monitoring
- metaspace 사용량은 GC log output을 통해서 확인 가능합니다.
- Jstat * JVirtualVM 은 아직 metaspace를 monitoring 하지 못합니다. 아직 예전 PermGen space reference를 확인하도록 되어 있습니다.
Heap Space
Java Heap Space는 Java Runtime에서 Memory를 Object 및 JRE Class에 할당하는 데 사용됩니다. 객체를 만들 때마다 항상 Heap Space에 만들어집니다.
Garbage Collection은 Heap Memory에서 실행되어 참조가 없는 Object가 사용하는 Memory를 해제합니다. Heap Space에서 생성 된 모든 개체는 전역 액세스 권한을 가지며 응용 프로그램의 어느 곳에서나 참조 할 수 있습니다.
Stack Memory
Java Stack Memory는 Thread 실행에 사용됩니다. 여기에는 수명이 짧은 Method 별 값과 Method에서 참조되는 Heap의 다른 Object에 대한 참조가 포함됩니다.
Stack Memory는 항상 LIFO(Last-In-First-Out) 순서로 참조됩니다. Method가 호출 될 때마다 Method가 로컬 기본 값을 보유하고 Method의 다른 Object에 대한 참조를 보유 할 수 있도록 Stack Memory에 새 블록이 작성됩니다.
Method가 종료되자마자 블록은 사용되지 않고 다음 Method에 사용할 수 있게 됩니다. Stack Memory 크기는 Heap Memory에 비해 매우 작습니다.
Java 프로그램의 힙 및 스택 메모리
public class Memory {
public static void main(String[] args) { // Line 1
int i=1; // Line 2
Object obj = new Object(); // Line 3
Memory mem = new Memory(); // Line 4
mem.foo(obj); // Line 5
} // Line 9
private void foo(Object param) { // Line 6
String str = param.toString(); //// Line 7
System.out.println(str);
} // Line 8
}
}
아래 이미지는 위 프로그램을 참조한 스택 및 힙 메모리와 기본, 객체 및 참조 변수를 저장하는 데 사용되는 방법을 보여줍니다.
-
프로그램을 실행하면 모든 런타임 클래스를 힙 공간으로 로드합니다. main() 메소드가 1행에서 발견되면 Java 런타임은 main() 메소드 스레드에서 사용할 스택 메모리를 작성합니다.
-
Line 2에서 로컬 변수를 만들고 있으므로 main()메소드의 스택 메모리에 만들어 저장됩니다.
-
Line 3에서는 객체를 생성하고 있기 때문에 힙 메모리에 생성되며 스택 메모리에는 이에 대한 참조가 포함됩니다. Line 4에서 Memory 객체를 만들 때 비슷한 프로세스가 발생됩니다.
-
Line 5에서 foo() 메소드를 호출하면 스택 맨 위에 있는 블록이 foo() 메소드에서 사용되도록 작성됩니다. Java는 값으로 전달되므로 Line 66의 foo() 스택 블록에 Object에 대한 새로운 참조가 작성됩니다.
-
문자열은 Line 7에서 만들어지고, 힙 공간의 String Pool에 들어가고 이에 대한 foo() 스택 공간에 참조가 만들어집니다.
-
foo() 메소드는 Line 8에서 종료되며, 이때 스택의 foo()에 할당 된 메모리 블록은 사용 가능해집니다.
-
Line 9에서 main() 메소드가 종료되고 main() 메소드에 대해 작성된 스택 메모리가 손상됩니다. 또한 프로그램은 이 행에서 종료되므로 Java Runtime은 모든 메모리를 해제하고 프로그램 실행을 종료합니다.
힙 메모리와 스택 메모리의 차이점
-
힙 메모리는 응용 프로그램의 모든 부분에서 사용되는 반면 스택 메모리는 하나의 실행 스레드에서만 사용됩니다.
-
객체가 생성 될 때마다 항상 힙 공간에 저장되며 스택 메모리에는 객체에 대한 참조가 포함됩니다. 스택 메모리에는 로컬 프리미티브 변수와 힙 공간의 객체에 대한 참조 변수 만 포함됩니다.
-
힙에 저장된 객체는 전역 적으로 액세스 할 수있는 반면 다른 스레드는 스택 메모리에 액세스 할 수 없습니다.
-
스택의 메모리 관리는 LIFO 방식으로 수행되는 반면, 힙 메모리에서는 전체적으로 사용되므로 더 복잡합니다. 힙 메모리는 Java Garbage Collection 에서 자세한 내용은 Young-Generation, Old-Generation 등으로 구분됩니다.
-
스택 메모리는 수명이 짧은 반면 힙 메모리는 시작부터 애플리케이션 실행이 끝날 때까지 지속됩니다. -Xms 및 -Xmx JVM 옵션을 사용하여 시작 크기 및 최대 힙 메모리 크기를 정의 할 수 있습니다 . -Xss 를 사용하여 스택 메모리 크기를 정의 할 수 있습니다.
-
스택 메모리가 가득 차면 Java 런타임이 발생 java.lang.StackOverFlowError하지만 힙 메모리가 가득 차면 java.lang.OutOfMemoryError: Java Heap Space오류 가 발생합니다. 스택 메모리 크기는 힙 메모리와 비교할 때 매우 작습니다. 메모리 할당 (LIFO)의 단순성으로 인해 스택 메모리는 힙 메모리와 비교할 때 매우 빠릅니다.
메소드로 전달할 수 있는 익명 함수(Anonymous Function)를 단순화한 것이라고 할 수 있습니다. 람다 표현식에는 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트는 가질 수 있습니다.
람다의 특징
- 익명
보통의 메소드와 달리 이름이 없으므로 익명이라 표현합니다.
- 함수
람다는 메소드처럼 특정 클래스에 종속되지 않으므로 함수라고 부릅니다. 하지만 메소드처럼 파라미터 리스트, 반환 형식, 가능한 예외 리스트를 포함합니다.
- 전달
람다 표현식을 메소드 인수로 전달하거나 변수로 저장할 수 있습니다.
- 간결성
익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없습니다.
람다의 구성
ex) (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
- 파라미터 리스트
Comparator의 compare 메소드 파라미터(사과 두개)
- 화살표
화살표(->)는 람다의 파라미터 리스트와 바디를 구분합니다.
- 바디
두 사과의 무게를 비교합니다. 람다의 반환값에 해당하는 표현식입니다.
람다 표현식
- () -> {}
- () -> "Seoul"
- () -> {return "Seoul"}
- (Integer a) -> {return "Seoul" + a;}
return을 사용하면 반드시 중괄호를 사용해야 합니다.
람다를 사용할 경우에는 반드시 함수형 인터페이스를 통해서 사용할 수 있습니다.
함수형 인터페이스
함수형 인터페이스는 정확히 하나의 추상 메소드를 지정하는 인터페이스입니다. Java 8 라이브러리 설계자들은 java.util.function 패키지로 여러가지 새로운 함수형 인터페이스를 제공합니다.
- Predicate : test 라는 추상 메소드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 boolean을 반환합니다. 따로 정의할 필요 없이 바로 사용할 수 있다는 점이 특징입니다.
ex) Predicate example source
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
public <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for(T t : list) {
if (p.test(t)) {
results.add(t);
}
}
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
- Consumer : 제네릭 형식 T 객체를 받아서 void를 반환하는 accept 라는 추상 메소드를 정의합니다. T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있습니다.
ex) Consumer example source
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
public <T> void forEach(List<T> list, Consumer<T> c) {
for(T t : list) {
c.accept(t);
}
}
forEach(
Arrays.asList(1, 2, 3, 4, 5);
(Integer i) -> System.out.println(i);
);
- Function<T, R> : 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메소드 apply를 정의합니다. 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있습니다(예를 들면 사과의 무게 정보를 추출하거나 문자열을 길이와 매핑).
ex) Function example source
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
public <T, R> List<R> map(List<T> list, FUnction<T, R> f) {
List<R> result = new ArrayList<>();
for(T t : list) {
result.add(f.apply(t));
}
return result;
}
// [7, 2, 6]
List<Integer> l = map(
Arrays.asList("lambdas", "in", "action"),
(String s) -> s.length() // Function의 apply 메소드를 구현하는 람다
);
- Supplier : 인수 없이 값을 리턴하는 함수를 의미합니다.
ex) Supplier example source
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Supplier<Integer> solution = () -> 42;
System.out.println(solution.get());
위의 네 개의 제네릭 함수형 인터페이스 외에 특화된 형식의 함수형 인터페이스도 있습니다. 자바의 모든 형식은 참조형 아니면 기본형에 해당하지만 제네릭의 내부구현 때문에 제네릭 파라미터에는 참조형만 사용할 수 있습니다. 자바에서는 기본형을 참조형으로 변환하는 기능을 제공하며 이 기능을 박싱이라고 합니다. 참조형을 기본형으로 변환하는 반대 동작을 언박싱이라고 합니다. 일반적으로 특정 형식을 입력으로 받는 함수형 인터페이스의 이름 앞에는 DoublePredicate, IntConsumer, IntFunction 처럼 형식명이 붙습니다.
@FunctionalInterface
@FunctionalInterface는 함수형 인터페이스임을 가리키는 어노테이션입니다. @FunctionalInterface로 인터페이스를 선언했지만 실제로 함수형 인터페이스가 아니면 컴파일 타임 에러를 발생시킵니다. 어떤 인터페이스들은 우연히 함수형으로 정의될 수도 있기 때문에, Functional Interface들이 모두 @FunctionalInterface 어노테이션으로 선언될 필요도 없고 그렇게 하는 것이 바람직하지도 않습니다. 즉, @FunctionalInterface는 작성한 인터페이스가 Functional Interface임을 확실히 하기 원할 때에만 사용하면 됩니다.
- 메소드 레퍼런스를 이용하면 기존의 메소드 정의를 재활용해서 람다처럼 전달할 수 있으며 람다 표현식을 사용하는것보다 가독성이 좋게 처리할 수 있습니다.
메소드 레퍼런스를 만드는 방법
- 정적 메소드 레퍼런스
ex) Integer의 parseInt 메소드는 Integer::parseInt로 표현가능
- 다양한 형식의 인스턴스 메소드 레퍼런스
ex) String의 length 메소드는 String::length로 표현가능
- 기존 객체의 인스턴스 메소드 레퍼런스
ex) Transaction 객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transaction 객체에는 getValue 메소드가 있다면,
이를 expensiveTransaction::getValue라고 표현가능
- 생성자 레퍼런스
ex) Apple(Integer weight)라는 시그니처를 갖는 생성자는 Function 인터페이스의 시그니처와 같아 다음과 같이 구현할 수 있습니다.
Function<Integer, Apple> c = Apple::new; Apple apple = c.apply(110); // Function의 apply 메소드에 무게를 인수로 호출해서 새로운 Apple 객체를 만들 수 있습니다.
RuntimeException을 상속하지 않는 클래스는 Checked Exception RuntimeException을 상속한 클래스는 Unchecked Exceptin으로 분류할 수 있습니다.
Checked-Exception과 Unchecked-Exception의 가장 명확한 구분 기준은 '꼭 처리를 해야 하느냐'입니다. Checked-Exception이 발생할 가능성이 있는 메소드라면 반드시 로직을 try/catch로 감싸거나 throw로 던져서 처리해야 합니다.
반면에 Unchecked-Exception은 명시적인 예외처리를 하지 않아도 됩니다. 이 예외는 피할 수 있지만 개발자가 부주의해서 발생하는 경우가 대부분이고, 미리 예측하지 못했던 상황에서 발생하는 예외가 아니기 때문에 굳이 로직으로 처리를 할 필요가 없도록 만들어져 있습니다.
또한 예외를 확인할 수 있는 시점에서도 구분할 수 있습니다. 일반적으로 컴파일 단계에서 명확하게 Exception 체크가 가능한 것을 Checked-Exception이라 하며, 실행과정 중 어떤 특정 논리에 의해 발견되는 Exception을 Unchecked-Exception이라 합니다.
한가지 더 인지하고 있어야 할 것은 예외발생 시 트랜잭션의 rollback 여부입니다. 기본적으로 Checked-Exception은 예외가 발생하면 트랜잭션을 rollback 하지 않고 예외를 던져줍니다. 하지만 Uncehcked-Exception은 예외 발생 시 트랜잭션을 rollback 한다는 점에서 차이가 있습니다.
예외 처리 방법
예외 처리 방법에는 예외가 발생하면 다른 작업 흐름으로 유도하는 예외 복구와 처리 하지 않고 호출한 쪽으로 던져버리는 예외처리 회피, 그리고 호출한 쪽으로 던질 때 명확한 의미를 전달하기 위해 다른 예외로 전환하여 던지는 예외 전환이 있습니다.
-
예외 복구
int try = TRY_CNT; while (try-- > 0) { try { // 예외가 발생할 가능성이 있는 시도 return; // 성공 시 리턴 } catch (Exception e) { // 로그 출력, 에러 처리 } finally { // 리소스 반납, 정리 } } throw new RetryFailedException(); // 최대 재시도 횟수를 넘기면 직접 예외 발생
try에서 예외가 발생할 수 있는 코드를 진행하고 예외 발생시 catch문에서 처리, 복구 시도를 하는 방법입니다. finally는 성공, 실패의 두가지 경우 모두 실행되는 부분입니다. 예외복구의 핵심은 예외가 발생하여도 애플리케이션은 정상적인 흐름으로 진행된다는 것입니다.
-
예외처리 회피
public void add() throws SQLException { ... // 구현 로직 }
예외가 발생하면 throws를 통해 호출한쪽으로 예외를 던지고 그 처리를 회피하는 것입니다. 호출한 쪽에서 예외를 받아 처리하도록 하거나, 해당 메소드에서 이 예외를 던지는 것이 최선의 방법이라는 확신이 있을 때만 사용해야 합니다.
-
예외 전환
catch(SQLException e) { ... throw DuplicateUserIdException(); }
예외전환은 위 코드처럼 예외를 잡아서 다른 예외로 던지는 것입니다. 호출한 쪽에서 예외를 받아서 처리할 때 좀 더 명확하게 인지할 수 있도록 돕기 위한 방법입니다. 어떤 예외인지 분명해야 처리가 수월해지기 때문입니다.
@Service
- 비즈니스 논리 및 호출 메소드를 정의
- 비즈니스 로직을 가지고 있음을 표현
- Business Layer에 속함 -> 컨트롤러와 뷰를 연결
- 필요할 때 특정 예외, 예외처리를 추가 할 수 있음
@Component
- 자동으로 스캔해서 등록하고 싶은 것들을 위해 사용
- @Component의 구체화된 형태로 @Controller, @Service, @Controller 등이 있음
- 클라이언트의 요청이 DispatcherServlet에 전달
- DispatcherServlet은 HandlerMapping을 사용하여 클라이언트의 요청을 처리할 컨트롤러 객체를 구함
- DispatcherServlet은 컨트롤러 객체의 handleRequest() 메서드를 호출하여 클라이언트의 요청을 처리
- 컨트롤러의 handlerRequest() 메서드는 처리 결과 정보를 담은 ModelAndView 객체를 리턴
- DispatcherServlet은 ViewResolver로부터 응답 결과를 생성할 뷰 객체를 구함
- 뷰는 클라이언트에 전송할 응답을 생성
Spring MVC에서 필요한 빈들을 등록하기 위해 사용합니다.
@EnableWebMvc는 어노테이션 기반의 Spring MVC를 구성할 때 필요한 Bean 설정들을 자동으로 해주는 어노테이션입니다.
또한 기본적으로 등록해주는 Bean들 이외에 추가적으로 개발자가 필요로 하는 Bean들을 등록을 손쉽게 할 수 있도록 도와줍니다.
-
SSL 클라이언트 컴퓨터가 자신의 버전, 암호 알고리즘 목록, 그리고 사용 가능한 압축 방식을 "client hello" 메시지에 담아 서버로 보냅니다.
-
SSL 서버는 클라이언트가 제공한 목록에서 서버가 선택한 암호 알고리즘, 선택한 압축 방식과 세션 ID 및 CA(Certificate Authority)가 사인한 서버의 공개 인증서를 "server hello" 메시지에 담아 응답합니다. 이 인증서는 대칭키가 생성되기 전까지 클라이언트가 나머지 handshake 과정을 암호화하는 데에 쓸 공개키를 담고 있습니다.
-
SSL 클라이언트는 서버의 디지털 인증서가 유효한지 신뢰할 수 있는 CA 목록을 통해 확인합니다.
-
만약 CA를 통해 신뢰성이 확보되면 클라이언트는 의사 난수(pseudo-random) 바이트를 생성해 서버의 공개키로 암호화합니다. 이 난수 바이트는 대칭키를 정하는 데에 사용되며 이 대칭키는 나중에 메시지 데이터를 암호화하는 데 사용됩니다.
-
SSL 서버가 "클라이언트 인증서 요청"을 보낸 경우 클라이언트는 클라이언트의 디지털 인증서 또는 "디지털 인증서 없음 경고" 와 함께 클라이언트의 개인 키로 암호화 된 임의의 바이트 문자열을 보냅니다. 이 경고는 경고일 뿐이지만 일부 구현에서 클라이언트 인증이 필수일 경우 handshake 실패합니다.
-
서버는 클라이언트의 인증서를 확인합니다. 난수 바이트를 자기 개인키로 복호화해 대칭 마스터키 생성에 활용합니다.
-
클라이언트는 handshake의 클라이언트 부분이 완료되었음을 알리는 Finished 메시지를 서버에 보내면서 지금까지의 교환 내역을 해시한 값을 대칭키로 암호화하여 담습니다.
-
서버는 스스로도 해시를 생성해 클라이언트에서 도착한 값과 일치하는지 봅니다. 일치하면 서버도 마찬가지로 대칭키를 통해 암호화한 Finished 메시지를 클라이언트에게 보냅니다.
-
이후부터 SSL 세션 동안 서버와 클라이언트는 대칭키로 암호화된 어플리케이션(HTTP) 데이터를 주고 받을 수 있습니다.