자바 성능 튜닝 이야기
디자인 패턴 꼭 쓰자
대표적으로 MVC 패턴이나 J2EE에서 사용된 패턴이 나온다.
뷰는 결과를 보거나 입력하는 화면 (frontend layer)
모델은 입력되어 치리된 내용을 저장, 관리, 수정한다. (DB)
컨트롤러는 이 둘을 이어주는 역할.
MVC 패턴이 좋은 디자인 패턴인 예로는 JSP 모델1을 예로 뷰와 비즈니스 모델의 분업화가 힘들어 사람에 따라 코드의 변경이 많이 일어날 수 있다. 모델 1은 JSP로 요청을 직접하지만 2는 서블릿으로 요청을 한다. 성능적 차이는 없더라도 협업과 코드유지보수의 관점에서 이점이 있다.
디자인 패턴은 의미 있는 클래스들을 묶은 각각의 집합을 이른다.. 이러한 집합들을 구조적으로 배치한 것.
J2EE 패턴에 괴상한 패턴들이 되게 많다 .. 이걸 다 공부하라는 건 당장 어려운 일 같다. 중점적으로 언급된 패턴만 다루자.
- Transfer Object 패턴
-
- 한 번에 여러 타입을 가지는 클래스를 사용해 하나의 값씩 여러번 요청하는 경우를 방지하자.
게터,세터 없는 편이 성능엔 조금 더 좋지만 다른 곳에서 임의적으로 수정되는 것을 막기위해선 private과 게터, 세터를 사용하는 것이 좋아보인다. 적어도 toString은 구현하자 게터세터를 안쓴다면
- 한 번에 여러 타입을 가지는 클래스를 사용해 하나의 값씩 여러번 요청하는 경우를 방지하자.
- Service Locator 패턴
-
- 얼핏 들었을 때 캐시 기능이 떠올랐다. 리버스 프록시 라던가.
- Business Delegate
-
- presentation 계층과 business 계층을 분리하는 데 사용된다. 계층 간 결합을 줄이고, 서비스의 구현 세부 정보를 숨긴다. 컨트롤러와 서비스를 구분한 게 이런 느낌인가 실제 로직은 서비스에 있으니 구현 로직을 숨기지 않나. 그냥 클라이언트에 서비스 로직이 들어가지 않게 구분한 것 자체가 해당되는 것처럼 들리기도 한다. 다른 사람이 구현한 코드를 보니 비즈니스 로직에 접근가능하게 하는 룩업인터페이스를 하나 두고 이 룩업인터페이스를 사용하는 대리자를 앞에 둔다.
- Session Facade
-
- 원격 클라이언트에 비즈니스 구성 요소 및 서비스를 제공할 때 직접 access 할 수 없도록 하거나 비즈니스 레이어에 원격 ㅁaccess 계층을 지공할 때, 서비스를 집계하여 클라이언트에 노출할 때, 노출되는 비즈니스 논리를 중앙화하고 집계할 때, 비즈니스 구성 요소와 서비스 간 복잡한 상호작용, 상호 종속성을 숨겨 관리 용이, 논리 중앙화로 유연성 높이고, 변경 사항에 대처, 한 마디로 비즈니스 계층 구성요소를 캡슐화하고 , coarse-grained(집약시킨)한 서비스를 원격 클라이언트에 노출한다. 클라이언트는 비즈니스 구성 요소에 직접 접근하지 않고 Session Facade에 접근한다.
- Data Access Object
-
- 비즈니스 로직과 DB를 분리하기 위한 패턴. DB를 사용하는 방법이 바뀌어도 클라이언트에선 변경되지 않도록 DB로직을 캡슐화하ㅁ여 분리
DataAccess Object Interface: 모델 객체에서 수행할 표준 작업을 정의하는 인터페이스
Data Access Object concrete class: 인터페이스 구현 클래스. 데이터소스에서 데이터를 가져오는 역할
꼭 B와 접근으로 한정지을께 아니라 레이어간 데이터를 다루는데 있어서 변경이 잦을 수 있지만 클라이언트에서는 변경을 줄이고 싶을 때 사용하면 될듯한다. (이건 DTO가 하는건가?)
VO: da0 클래스를 사용하여 검색된 데이터를 저장하기 위한 get/set 메서드 포함 간단한 POJO 객체이다.
Spring에서의 repository가 DAO의 역할을 하는 것인가? 실질적 쿼리나 그런걸 다 맡던데
- 비즈니스 로직과 DB를 분리하기 위한 패턴. DB를 사용하는 방법이 바뀌어도 클라이언트에선 변경되지 않도록 DB로직을 캡슐화하ㅁ여 분리
Adapter, Decorator, Facade, Proxy 패턴은 Composition, Delegation 관점에서 같다
Adapter는 interface를 감싸서 호출을 위임하고,
Decorator는 object를 감싼 동작을 구현한다.
Facade는 하나 이상의 서비스를 중앙화한 인터페이스를 감싸서 위임한다
Proxy는 Subject를 감싸고 호출을 위임한다.
얼핏보기 다비슷비슷한데 뭐가 다른겨
답은 의도에 있다한다
Adapter의 목적은 interface 전환이다. 이는 두 개의 컴포넌트가 같이 동작하게 한다. 하나의 인터페이스를 구현하는 컴포넌트로
Decorator는 실행시점에 새로운 기능을 추가한다. 생성 후에도 object의 기능을 추가할 수 있게 enrich한다
Facade는 interface 를 제공해서 클라이언트가 비즈니스 시스템에 직접접근하는것을 막는 것이다. 이 때 인터페ㅇ이스는 관련된 서비스들을 집약할 수 있다. (중앙 집약화)
Proxy는 Adapter, Decorator와 비슷하지만 object 접근제어가 목적. 직접ㅈ적 접근을 막으며 실제 object처럼 동작.실제 오브젝트의 동작을하고나 따로 정의한 동작을 하게할 수있다. 다목적한 패턴이다. 원격 object와 통신하는 remote proxy, 비용이 많이 드는 object에 접근을 통제하는 Virtual proxy, role 기반 object의 접근을 제공하는 Protection proxy, 캐시 objet를 반환하는 Caching proxy 등
이들 사이에서의 차이점이라면
- Adpater는 인터페이스를 전환하지만, Decorator는 인터페이스를 전환하지는 않고 단지 원본 object를 받아들이는 메소드에 전달될 수 있도록 원본 object의 인터페이스를 구현할 뿐이다.
- Decorator 패턴과 Proxy의 주된 차이는 Decorator는 절대 object를 생성하지 않고, 항상 존재하는 object에 새 기능을 추가한다. proxy는 object가 존재하징 ㅏㄶ으면 object를 생성할 수 있다.
- Decorator 패턴은 여러 decorator를 연결(해서 여러 기능을 추가할 수 있고 순차적으로 동작시킬 수 있지만 proxy는 proxy끼리의 연결을 권하지 않는다
- Facade는 Decorator와 같지 않다 facade는 새로운 기능의 추가없이 기준 기능을 제공한다.
- proxy나 adapter 패턴은 특정 interface를 구현한 facade를 요구하지 않는다. facade는 단순 개별 서브 시스템 컴포넌트를 내부에 유지하고 클라이언트가 요구한 간단한 동작만 제공한다. 클라이언트에서 car.init()을 하면 내부적으로 배터리, 엔진 등의 클래스에서 동작이 일어날 수 있다. (메소드가 호출될 수 있다)
두 개의 다른 부분을 같이 동작하게 할 interface로 변환해야 한다면 Adapter
보안이나 성능, 네트워킹 등의 이류로 실제 object를 숨겨야 한다면 Proxy
런타임으로 기존 object에 새로운 동작을 추가해야 한다면 Decorator(클라이언트 요청에 따라 다른 순서로 동작을 섞어서 적용할 수 있는 유연함 제공)
마지막으로 클라이언트가 복잡한 시스템에 단순하게 접근하게 하려면 facade 패턴
성능 측정
프로파일링 툴
- 소스 레벨의 분석을 위한 툴
- 메모리 사용량을 객체나 클ㄹ래스, 소스의 라인 단위까지 분석
APM 툴
- 애플리케이션의 장애 상황에 대한 모니터링ㅇ 및 문제점 진단이 주 목적
- 실시간 모니터링을 위한 툴
System 내 운영중 코드에 사용하면 안되는 함수
- static void gc() : 자바에서 사용하는 메모리를 명시적으로 해제하도록 GC를 수행
- static void exit(int status) 수행중인 VM을 멈춘다. 절대 수행 NO 성능 구더기?
- static void runFInalization(): Object 객체에 finalize 메서드는 자동으로 호출되는데 제거될 떄 사요되는 것 gc에 의해서 하지만 이걸 쓰면 수동으로 수행해야한다.. 니가 C야?!!!!!!!!
기존엔 System.currentTimeMills()를 써서 시간을 알아왔지만
JDK1.5부턴 System.nanoTime()을 쓰자네
성능차이가 그렇게 큰지는 모르지만 초기 성능이 느린건 클래스가 로딩될 때의 성능저하와 JIT Optimizer가 작동하면서 성능최적화가 일어나기 때문이기도 하다.
책에선 JMH라는 툴을 이용하여 프로파일링을 진행한다..
String, builder, buffer
String을 써서 합하게 되면 매번 의미없이 객체가 생성되고 GC에 의해 삭제된다. 오버헤드다.
비즈니스에 따라 이러한 문자열 합을 몇만번 더하는 경우도 있다한다(회계 쿼리라나)
그러니 StringBuilder, StringBuffer를 사용하자 Buffer는 쓰레드세이프하고 Builder는 그렇지않다.
jDK5.0부턴 String으로 합하면 스트링빌더로 바꿔준다. 최적화 요소 그 이전버전도 스트링끼리의 합은 컴파일단계에서 합쳐준다. 중간에 다른 타입끼면 불가
콜렉션
Collection, Map 인터페이스의 이해
Collection 아래 Set, List, Queue(jdk 5)
Set 아래 SortedSet
Map 아래 SortedMap
Collection: 가장 상위 인터페이스
- Set: 중복을 허용하지 않는 집합을 처리하기 위한 인터페이스이다.
- SortedSet: 오름차순을 갖는 Set 인터페이스
- List: 순서가 있는 집합을 처리하기 위한 인터페이스이기 때문에 인덱스가 있어 위치를 지정하여 값을 찾을 수 있다. 애용은 ArrayList애용
- Queue: 여러 개의 객체를 처리하기 전에 담아서 처리할 때 사용하기 위한 인터페이스이다. 기본적으로 FIFO
- Map: Map은 키와 값의 쌍으로 구성된 객체의 집합을 처리하기 위한 인터페이스. 중복 키 허용 X
- SortedMap: 키를 오름차순으로 정렬하는 Map 인터페이스
Set 인터페이스를 보자. 중복이 없는 집합객체를 만들 떄 유용.
구현 클래스로는
- HashSet: 데이터를 해쉬 테이블에 담는 클래스로 순서 없이 저장
- TreeSet: red-black이라는 트리에 데이터를 담는다. 값에 따라 순서가 정해진다. 데이터를 담으며 동시에 정렬. HashSet보다 성능 상 느리다.
- LinkedHashSet: 해쉬 테이블에 데이터를 담는데, 저장된 순서에 따라 순서가 결정된다.
red-black 트리는 이진트리구조로 다음과 같은 특징이 있다. - 각 노드는 검윽색이나 붉은색
- 가장 상위는 검은색
- 가장 말단은 검은색
- 붉은 노드는 검은 하위만 가진다. (검은 노드는 붉은 상위만 가진다)
- 모든 말단 노드로 이동하는 경로의 검은 노드 수는 동일하다.
List 인터페이스를 구현한 클래스.
- Vector: 객체 새엉 시에 크기를 지정할 필요없는 배열 크래스(동기화처리됨)
- ArrayList: Vector와 비슷하지만, 동기화 처리 안됨
- LinkedList: ArrayList와 동일하지만, Queue 인터페이스를 구현하여 FiFO 큐 작업을 수행한다.
맵 구현 클래스로
- Hashtable: 데이터를 해쉬 테이블에 담는 클래스. 내부에서 관리하는 해쉬 테이블 객체가 동기화되어 있어, 동기화가 필요할때 써라
- HashMap: 데이터를 해쉬 테이블에 담는 클래스. Hashtable클래스의 다른 점은 null값을 허용한다는 것과, 동기화 되어 있지 않다는 것.
- TreeMap: red-black 트리에 데이터를 담는다. TreeSet과 다른 점은 키에 의해서 순서가 정해진다.
- LinkedHashmap: Hashmap과 거의 동일하며 이중 연결리스트 방식을 사용해 데이터를 담는다.
큐 왜 쓰냐. List도 순서있는데. 리스트의 단점은 데이터가 많은 경우 처리시간이 늘어난다. 맨 뒤에껄 삭제하는게 아니라면 모든 값을 앞으로 앞당겨오기에 이러한 부분에서 문제.
Queue 구현 클래스는 두개로 구분. 일반목적의 LinkedList, PriorityQueue. Java.util.concurrent 패키지의 애들이다.
- PriorityQueue: 큐에 추가된 순서와 상관없이 먼저 생성된 객체가 먼저 나오게 된 큐
- LinkedBlockingQueue: 저장되는 데이터의 크기가 정해져 있는 FiFO 기반의 블로킹 큐 (블로킹은 더이상 공간이 업을때 생길떄까지 대기하는 녓ㄱ)
- ArrayBlockingQueue: 저장되는 데이터의 크기가 정해져 있는 FIFO 기반의 블로킹 큐
- PriorityBlockingQueue: 저장되는 데이터의 크기가 정해져 있지 않고, 객체의 생성순서에 따라서 순서 저장.
- DelayQueue: 큐가 대기하는 시간을 지정하여 처리하도록 되어 있는 큐
- SynchronousQueue: put() 메서드를 호출하면 다른 스레드에서 take() 호출 전까지 대기하도록 된 큐. 이 큐에는 저장되는 데이터가 없다. 제공 메서드는 0이나 null 리턴
Set 중 젤 빠른것
add 시 HashSet, LinkedHashSet이 빠르고 TreeSet은 그 뒤이다. 삽입 시 정렬이 이루어지기 떄문이다. 크기를 정해놓고 진행하면 좀 더 빠르다.
값을 꺼낼 때 HashSet, LinkedHashSet이 빠르다. TreeSet은 값을 뺴면서도 정렬이 일어난ㄷ...
List 중 젤 빠른 것 (중간 삽입 불가)
add 시 ArrayList, Vector, LinkedList 큰 차이없다
값을 꺼낼 때 ARrayList의 속도가 가장 빠르고, Vector와 LinkedList는 속도가 매우느리다. LinkedList가 유독 느린건 Queue인터페이슬ㄹ 상속받아서이다. 동시성라이브러리 관련된 문제인가. 이를 수정 위해 순차적 결과 받아오는 peek() 메서드를 사용한다네.
ArraylIst는 여전히 빠르고 그 다음 LinkedList, Vector 순으로 바뀐다. LinkedList 사용할 떈 peek(), poll() 쓰라네 이게 큐 관련 APIㅇㄴ가봐
ArrayList와 Vector 사이엔 왜 차이가 클까. get에 synchronized가 선언되어있다네..
삭제 시
ArrayLast를 삭제할 때 젤 빠르고 그외엔 다 비슷비슷하다 또 값이 커지면 달라질듯하다. Linked가 분산적게 빨라야하지 않을까.
Map 중 젤 빠른 녀석
get으로 가져올 떄
SeqHashMap , RandomHashMap, SeqLinkedHashMap, RandomLinkedHashmap 이 빠르고
Seqhashtable, RandomHashtable 그 다음
SeqTreeMap, RandomTreeMap이 그 다음이다.
트리가 아무래도 느리다. 구조특성 상
Set은 HashSet 쓰고 List ArrayList, Map HashMap, Queue LinkedList 쓰자 sun이 이러라네
JDK 1.0 버전 Vector, Hashtable은 동기화 처리됨, 1.2 이후 애들은 안됨.
최신 버전 클래스들의 동기화 지원을 위해 synchronized로 시작하는 애들도 있다.
근데 이거한다고 그렇게 막 효과적으로 바뀌진 않는다. 성능 기준에 안미치면 프로파일링하고 병목부분을 개선해야한다.
반복문 튜닝
조건문의 수에 의한 성능저하는 그렇게 크지않다. 조건문의 비교과정에서의 걸리는 시간이 더 중요한 부분 같다.
JDK7 부턴 String도 비교가 가능한데 이건 toString으로 해쉬코드를 가져와 비교할 수 있기 때문이다.
반복문에서 for, while, do-while 있는데 while은 자칫하면 무한루프 가능하니 for를 쓴다네.. 그냥 가독성 고려해서 생각해야겠다.
JDK5부턴 For-Each 가능 첫 번째 원소부터 순차적으로 접근하기에 좋다. get, elementAt 같은 메서드 사용할 필요 없어서 좋아.
반복문 내에서 낭비적으로 함수가 호출되게 하지말자. 기존에 한 번만 호출하면 된다면 반복문 이전에 변수에 할당한 뒤 사용하자.
static 제대로 써볼까?!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
성능을 개선하는데 좋은 방법으로 static을 사용하는것이 될 수 있다.
스태틱 선언이 된 변수는 클래스의 변수가 되어 인스턴스를 아무리 생성을 해도 하나의 클래스 변수를 가지게 된다.
더군다나 스태틱 블럭이란 것도 있다. 클래스가 로딩될 때 최초에 실행이 되며 내부에 변수를 둘 수동 ㅣㅆ다. 스태틱 블럭을 여러군데 작성이 가능하며 순서대로 진행된다. static의 특징으로 다른 jvm에서는 static이라고 선언해도다른 주소나 다른 값을 참조하지만, 하나의 jvm이나 was인스턴스에서는 같은 주소에 존재하는값을 참조한다는 것이다. 그리고 갈비지콜렉터의 대상에서도 제외된다. 그러므로 static을 잘 사용하면 성능을 뛰어나게 향상시킬 수 있지만, 잘못 사용하면 예기치 못한 결과를 초래하게 된다. 웹 환경에서 static을 잘못 사용하다가는 여러 쓰레드에서 하나의 변수에 접근할 수도 있기 때문에 데이터가 꼬이는 큰 일이 발생할 수 있다.
잘 사용하는 방법은 뭘까
자주 사용하고 절대 변하지 않는 변수는 final static으로 선언하자.
자주 변경되지 않고, 경우의 수가 단순한 쿼리 문장이 있다면 final static, static으로 선언하여 사용하자. 자주 사용되는 로그인 관련 쿼리들이나 간단한 목록조회 쿼리를 final static으로 선언하면 적어도 1바이트 이상의 객체가 GC대상에ㅓ 포함되지 않는다. 또한 JNDI 이름이나 간단한 코드성 데이터들을 static으로 선언해놓으면 편리하다. 간단한 데이터들도 stsatic으로 선언할 수 있지만, 템플릿 성격의 객체를 static으로 선언하는 것도 성능 향상에 많은 도움이 된다. Velocity를 사용할 때가 좋은 예이다. 이게 뭐지..?
Velocity는 자바 기반의 프로젝트를 수행할 때, UI가 될 수 있는 HTML 뿐만 아니라 XML , 텍스트 등의 템플릿을 정해놓고, 실행 시 매개변수 값을 던져서 원하는 형식의 화면을 동적으로 구성할 수 있도록 도와주는 컴포넌트이다.
Velocity 기반의 성능을 테스트해보면 템플릿을 읽어 오는 부분에서 시간이 가장 많이 소용된다.
템플릿 파일을 일어서 파싱하기 때문에 서버의 CPU에 부하가 많이 발생하고 대기 시간도 많아진다. 그러므로 수행하는 메서드에서 이 부분을 분리하여 다음과 같이 수정한다.
try {
Template template = Velocity.getTemplate("TemplateFIleName");
}
static Template template;
static {
try {
template = Velocity..getTemplate("TemplateFileName");
} catch (Exception ignored) {
}
}
이렇게 작성하면 화면을 요청할 때마다 템플릿 객체를 파싱하여 읽을 필요가 없다. 클래스가 로딩될 때 한 번만 파싱하므로 성능이 엄청나게 향상된다. 실제로 적용했을 때 부하 상황에서 평균 3초가 소요되던 화면이 0.5초로 단축된다. 하지만 template 내용이 지속적으로 변경되는 부분이라면 이와 같이 작성을 할 시 A 화면이 보여야 하는 사용자에게 B 화면이 빌 수 있어. 상황에 맞게 설정하는 것이 중요하다.
설정 파일 정보도 static으로 관리하자
요즘 자바 기반으로 개발할 때 보면 매우 많은 설정 파일들이 존재하는데, 클래스의 객체를 생성할 때마다 설정 파일을로딩하면 엄청난 성능 저하가 발생하게 된다. 이럴 때는 반드시 static으로 데이터를 읽어서 관리해야한다.
코드성 데이터는 B에서 한 번만 읽자
큰 회사의 부서 코드나 큰 쇼핑몰의 상품코드처럼 양이 많고 자주 바뀔확률이 노은 데이터를 제외하고, 부서가 적은 회사의 코드나, 건수가 그리 많지 않되 조희 빈도가 높은 코드성 데이터는 DB에서 한 번만 읽어서 관리하는 것이 성능측면에서 좋다 어떻게 사용해얗 하는지 다음의 예를 보자.
그냥 싱글톤으로 만들어서 static 블럭으로 초기화를 한다. 서로 다른 JVM에 올라갈 수 있다는 표현을 하는데 이게 무슨 의미인지 잘 몰곘다. java 하나 돌렸으면 그게 jvm에 올라가서 동작하는거 아닌가. 반으로 쪼개져서 다른 jvm에 들어가기라도 한단 말인가. 서버를 여러 데 뒀을 때를 염두해두고 하는 말인가? 이러한 JVM 간에 상이한 겨로가가 나오는 것을 방지하기 위해서 요즘에는 memcahed, EhCached 등의 캐시(cache)를 많이 사용한다.
static 잘못 쓰면 이렇게 된다
static으로 선언한 변수에 여러 쓰레드가 접근해서 값을 중간에 바꿔버리면 잘못된 결과를 받아올 수 있다.
static과 메모리 릭
static으로 선언한 부분은 GC의 대상이 되지 않는다. 그래서 지속적으로 쌓이게되면 OutOfMemoryError를 발생할 수 있다. 더 이상 사용 가능한 메모리가 없어지는 현상을 메모리 릭이라고 한다. static, Collection 객체를 잘못하용하면 메모리 릭이 발생한다. 메모리릭의 원인은 메모리 현재 상태를 (메모리의 단면ㅇ을)파일로 남기는 HeapDump라는 파일을 통해서 확인가능하다. JDK/bin 디렉터리에 있는 jmap이라는 파일을 사용하여 덤프를 남길 수 있다. 남긴 더덤ㅍ는 eclipse프로젝트에서 제공한느 AMT와 같은 툴을 통해서 분석하면 된다.
정리하며
static은 원리를 알고 사용하면 성능을 향상시킬 수 있는 마법의 예약어이다. 잘못사용하면 돌이킬 수 없는 일, 시스템이 다운되거나 예기ㅣㅊ 못한 결과가 나올 수 있다. static은 메모리에 올라가며 GC의 대상이 되지 않는다. 객체를 다시 생성한다고 해도 그 값은 초기화 되지않고 해당 클래스를 사용하는 모든 객체에서 공유하게 된다. 잘못쓸바엔 아예 안쓰는게 낫다.
클래스 정보 알아낼래!!!!
자바에는 클래스와 메서드의 정보를 확인할 수 있는 API가 존재. Class, Method 클래스이다. 자바 API에는 reflection이라는 패키지가 있다. 이 패키지에 있는 클래스들을 사용하면 JVM에 로딩되어 있는 클래스와 메서드 정보를 읽어올 수 있다.
Class 클래스
클래스에 대한 정보를 얻을 때 사용하기 좋고, 생성자는 따로 없다. ClassLoader 클래스의 defineClass()메서드를 이용해서 클래스를 객체를 만들 수도 있지만 좋은 방법은 아니다. 그보다는 Object 클래스에 있는 getClass()메서드를 이용하는게 일반적이다.
Class 클래스의 주요 메서드
- String getName(): 클래스의 이름을 리턴한다
- Package getPackage(): 클래스의 패키지 정보를 패키지 클래스 타입으로 리턴한다.
- Field[] getFields(): public으로 선언된 변수 목록을 Field 클래스 배열 타입으로 리턴한다.
- Field[] getDeclaredFields(): 해당 클래스에 정의된 변수 목록을 Field 클래스 배열 타입으로 리턴한다.
- Field getDeclaredField(String name): name과 동일한 이름으로 정의된 변수를 Field 클래스 타입으로 리턴한다.
- Method[] getMethods(): public으로 선언된 모든 메서드 목록을 Method 클래스 배열 타입으로 리턴한다. 해당 클래스에서 사용 가능한 상속받은 메서드도 포함된다.
- Method getMethod(String anme, Class ... parameterTypes): 지정된 이름과 매개변수 타입을 갖는 메서드를 ethod 클래스 타입으로 리턴한다.
- Method[] getDeclaredMethods(): 해당 클래스에서 선언된 모든 메서드 정보를 리턴한다.
- Method getDeclaredMethod(String name, Class... parameterTypes): 지정된 이름과 매개변수 타입을 갖는 해당 클래스에서 선언된메서드를 Method 클래스타입으로 리턴한다.
- Constructor[] getCOnstructors(): 해당 클래스에 선언된 모든 public 생성자의 정보를 Constructor 배열 타입으로 리턴한다.
- Constructor[] getDeclaredConstructors(): 해당 클래스에서 선언된 모든 생성자의 정보를 Constructor 배열 타입으로 리턴한다.
- int getModifiers(): 해당 클래스의 접근제어자 정보를 int 타입으로 리턴한다.
- String to String(): 해당클래스 객체를 문자열로 리턴한다.
method 클래스
Method 클래스를 이용하여 메서드에 대한 정보를 얻을 수 있다. 하지만, Method 클래스에는 생성자가 없으므로 Mehtod 클래스의 정보를 얻기 위해서는 Class 클래스의 getMethods() 메서드를 사용하거나 getDeclaredMethod() 메서드를 ㅆ야한다.
Method 클래스의 주요 메서드에 대해서 알아보자.
- Class<?> getDeclaringClass(): 해당 메서드가 선언된 클래스 정보를 리턴한다.
- Class<?> getReturnType(): 해당 메서드의 리턴 타입을 리턴한다.
- Class<?>[] getParameterTypes(): 해당 메서드를 사용하기 위한 매개변수의 타입들을 리턴한다.
- String getName(): 해당 메서드의 이름을 리턴한다.
- int getModifiers(): 해당 메서드의 접근자 정보를 리턴한다.
- Class<<?>[] getExceptionTypes(): 해당 메서드에 정의되어 있는 예외 타입들을 리턴한다.
- Object invoke(Object obj, Object... args): 해당 메서드를 수행한다.
- String toGenericString(): 타입 매개변수를 포함한 해당 메서드의 정보를 리턴한다.
- String toString(): 해당 메서드의 정보를 리턴한다.
Field 클래스
클래스에 있는 변수들의 정보를 제공하기 위한 클래스. 생성자 없다. Class 클래스의 getField() 메서드나 getDeclaredFields() 메서드를 사용해야한다. Field 클래스의 주요 메서드
- int getModifiers(): 해당 변수의 접근자 정보를 리턴한다.
- String getName(): 해당 변수의 이름을 리턴한다.
- String toString(): 해당 변수의 정보를 리턴한다.
나머지 refletion 관련 클래스는 앞에서 설명한 세 가지 클래스와 비슷하게 사용할 수 있다.
클래스 정보를 가져오는 부분과 이 책의 뒷부분에 있는 'JMX'를 연계시킨다면 서버에서 사용하는 클래스의 정보를 가져오는 막강한 모니터링 기술을 제공할 수 있다.
리플렉션을 잘못 쓴 예
일반적으로 로그를 프린트할 때 클래스 이름을 알아내기 위해서는 같이 Class 클래스를 많이 사용한다.
this.getclass().getName()
이 방법을 사용한다고 해서 성능에 많은 영향을 미치지는 않지만 getClass() 메서드를 호출할 때 Class 객체를 만들고 그 객체의 이름을 가져오는 메서드를 수행하는 시간과 메모리를 사용할 뿐이다. 하지만 어떤 개발자들은 relection 관련 클래스를 너무 좋아한 나머지 잘못 사용하는 경우도 간혹 있다.
클래스의 이름을 알아내기 위해서 getClass().getName() 메서드를 호출하여 사요했다. 이렇게 사용할 경우 응답속도에 그리 많은 영향을 주지는 않지만, 많이 사용하면 필요없느 시간을 낭비하게 된다. 이러한 부분에서개선이 필요할 떄는 자바의 기본으로 돌아가자.
getClass.getName으로 클래스 일치 조건을 구현하지말고 instanceof를 쓰자.. 이거 예약어인가
instanceof의 성능이 시간적으로 약 6배 더 빠르다. 시간적 차이가 그렇게 압독적이다라 할 순 없지만 개선할 수 있으면 개선하는게 좋다.
클래스의 메타데이터 정보는 JVM의 Perm영역에 저장된다. 이 영역이 사라졌었나..? 메타데이터에는 뭐갛 ㅐ당되지? Class 클래스를 사용해 많은 클래스를 동적으로 생성해버리면 OutOfMemoryError 가능
로그를 남길 때도 클래스 채로 생성하는거보단 클래스의 이름을 복사해서 붙여넣는 것이 소스를 확인할 떄도 편하다.
로그를 프린트할 때 클래스 정보까지는 나타낼 수 있지만, 메서드 정보까지 나타내기는 수비지가 않다. 그 방법은 '10. 로그는 반드시 필요한 내용만 찍자. 에 자세히 나온다.
synchronized는 제대로 알고 써야한다.
웹 기반 시스템을 개발할 때 쓰레드 컨트롤할 일 별로 없다. 만약 직접 건드리면 서비스의 안전성이 떨어질 수 있으니 자제하는 편이 좋다. 우리가 개발하는 WAS는 여러 개의 스레드가 동작하도록 되어있따. 그래서 synnchronized를 자주 사요한다. 하지만 sumchronized를 쓴다고 무조건 안정적인 것은 아니며 성능에 영ㅎㅇ을 미치는 부분도 있다. 스레드가 어떻게 작동되는지 간단하게 알아바ㅗ고, 무엇을 조심해야하는지 확인해보자.
스레드는 보통 잡 기본과정에서 배우고 나면 사용할 일이 별로없기 때문에 기억에서 사라진다. WAS를 만든다면 몰라. 대부분의 개발자가 그러한 일을 하는 것은 아니다. 그러니 기본적인 부분이라도 짚고가자.
프로세스와 스레드
클래스를 하나 수행시키거나 WAS를 가동하면, 서버에 자바 프로세스가 하나 생성된다. 하나가 생성되는지 여러 개가 생성되는 지는 윈도의 자원과리자나 리눅스, 유닉스의 프로세스를 조회해 보면 된다.
하나의 프로세스에는 여러 개의스레드가 생성된다. 단일 스레드가 생성되어 종료될 수도 있고 , 여러 개의 스레드가 생성되어 수행될 수도 있다. 그러므로 프로세스와 스레드의 관계느느 1:다 관계라고 보면 된다. 프로세스와 스레드는 왜 이러한 관계가 마늘어질까? 스레드는 다른말로 LightweightProcess(LWP)라고도 한다. 프로세스에서 만들어 사용하고 있는 메모리를 공유한다. 그래서 별개의 프로세스가 하나씩 뜨는 것보다는 성능이나자원 사용에 있어서 많은 도움이 된다. 프로세스와 스레드의 관계는 별로 어렵지도 않으니 반드시 숙지하고 있길 바란다.
Thread 클래스 상속과 Runnable 인터페이스 구현
스레드의 구현은 Thread 클래스를 상속받는 바법과 Runnable 인터페이스를 구현하는 방법 두가지가 있다. 기본적으로 Thread 클래스는 Runnable 인터페이스를 구현한 것이기 때문에 어느 것을 사용해도 거의 차이가 없다. 대신 Runnable 이너페이스를 구현하면 원하는 기능을 추가할 수 있다. 이는 장점이 될 수도 있지만 해당 클래스를 수행할 때 별도의 스레드 객체를 생성해야한다는 점은 단점이 될 수도 있다. 또한 자바는 다중 상속을 인정하지 않는다. 다라서 스레드를 사용해야할 때 이미 상속 받은 클래스가 존재한다면 Runnable 인터페이스를 구현해야한다.
Thread는 만들어놓고 .start 하면 되는 Runable은 impl 객체를 Thread 생성자에 넣은 후 만들어진 Thread를 start해야한다
순서는 따로 정해지지 않았다.
현재 진행 중인 스레드를 대기하도록 하기 위해서는 sleep(), wait(), join() 세가지 메서드를 사용하는 방법이 있다. wait() 메서드는 모든 클래스의 부모 클래스인 Object 클래스에 선언되어 있으므로 어떤 클래스에서도사용할 수 있다. 이 세 가지 메서드는 모두 예외를 던지도록 되어 있어 사용할 때는 반드시 예외 처리를 해주어야 한다.
sleep() 메서드는 명시된 시간만큼 해당 스레드를 대기시킨다. 이 메서드는 다음과 같은 두 가지 방법으로 매개변수를 지정해서 사용한다.
- sleep(long millis): 명시된 ms만큼 해당 스레드가 대기한다. static 메서드 이기 때문에 반드시 스레드 객체를 통하지 않아도 사용할 수 있다.
- slepp(long millis, int nanos): 명시된 ms + 명시된 나노 시간 만큼 ㅁ해당 스레드가 대기한다. 여기서 나소시간은 999999 까지 사용할 수 있다. 이 메서드도 위와 마찬가지로 static이다.
어떻게 시간을 정확하게 체크하는거지..
wait() 메서드도 명시된 시간 만큼 해당 스레드를 대기시킨다. sleep() 메서드와 다른 점은 매개변수인데, 만약 아무런 매개변수를 지정하지 않으면 notify() 메서드 혹은 notifyAll() 메서드가 호출될 때까지 대기한다. wait(0 메서드가 대기하는 시간을 설정하는 방법은 sleep()메서드와 동일하다.
join() 메서드는 명시된 시간만큼 해당 스레드가죽기를 기다린다. 만약 아무런 매개변수를 지정하지 않으면 죽을 때까지 계쏙 대기한다.
interrupt(), notify(), notifyAll() 메서드
앞에 명시한 세 개의 메서드를 '모두' 멈출 수 있는 유일한 메서드가 interrupt() 메서드다. 호출될 시 중지된 스레드에는 InterruptException이 발생한다. 제대로 수행되었는지 확인하려면 interrupted() 메서드를 호출하거나 isInterrupt()ed() 메서드를 호출하면 된다. 두 방법 차이는 interrupted() 메서드는 스레드의 상태를 변경하지만 후자는 그렇지 않다.
isAlive() 메서드라는 것도 있는 살아있ㅇ는 확인하는 용도이다.
notify() 메서드와 notifyAll() 메서드는 모두 wait() 메서드를 멈추기 위한 메서드다. 이 두 메서드는 Object 클래스에 정의되어 있고
wait() 메서드가 호출된 후 대기 상태로 바뀐 스렏르를 깨운다. notify() 메서드는 객체의 모니터와 관련있는 단일 스레드를 깨우며, notifyAll() 메서드는 객체의 모니터와 관련있는 모든 스레드를 깨운다.
간단한 예를 통해서 대기 메서드와 중단 메서드의 사용법을 확인해보자.
interrupt()는 절대적인게 아니야. 해당 스레드가 block 되거나 특정 상태에서만 작동한다. wait으로 대기 시키는 것
synchronized는 하나의 객체에 여러 객체가 동시에 접근하여 처리하는 상황이 발생할 떄 사용한다.
하나의 객체에 여러 요청이 동시에 달려들면 원하는 처리를 하지도 못하고 이상한 결과가 나올 수 있다. 그래서 synchronized를 사용해 동기화를 하는 것이다.
메서드와 블록으로 사용할 수 있고 스태틱과 같이 생각하면 머리가 아파온다.
식별자만 쓰면 간단하게 동기화할 수 있다.
언제 동기화를 사용해야할까
- 하나의 객체를 여러 스레드에서 동시에 사용할 경우
- static으로 선언한 객체를 여러 스레드에서 동시에 사용할 경우
이 경우가 아니면 사용할 경우가 없다.
동기화는 이렇게 사용한다. - 동일 객체 접근 시
건드릴 위험 있는 메서드의 시그니쳐에 추가한다. synchronized 다는 것 만으로도 시간적인 소모가 생긴다.
동기화는 이렇게 사용한다. - static 사용 시
클래스 변수에 접근하는데 synchronized method를 사용해 여러 인스턴스가 접근하면 동기화가 이뤄지지 않는다.
static synchronized method를 만들어야 인스턴스 단위가 아닌 클래스 자체에 해당시킬 수 있다.
동기화를 위해 자바에서제공하는 것들
JDK 5.0 부터 javautil.concurrent 패키지가 있다.
- Lock: 실행 중인 스레드를 간단한 방법으로 정지시켰다가 실행시킨다. 상호참조로 인해 발생하는 데드락을 피할 수 있다.
- Executors: 스레드를 더 효율적으로 관리할 수 있는 클래스들을 제공한다. 스레드 풀도 제공하니 필요에 따라 유용하게 사용 가능.
- Concurrent 콜렉션: 앞서 살펴본 콜렉션의 클래스들을 제공한다.
- Atomic 변수: 동기화가되어 있는 변수를 제공한다. 이 변수를 사용하면 synchronized 식별자를 메서드에 지정할 필요 없이 사용 가능
JVM 내에서 synchronization은 어떻게 동작할까?
HotSpotVM에선 자바 모니터를 제공한다. 스레드들이 상호배제프로토콜에 참여할 수 있도록 ㅗㄷㅂ는다. 자바 모니터는 잠긴 상태(lock) 풀림(unlocked) 중 하나이며 동ㅇ일한 모니터에 진입한 여러 스레드 중 한 시점에는 단 하나의 스레드만 모니터를 가질 수 있다. 이 후 작업이끝나면 다른 스레드에게 넘어간다.
JDK5 부터는 -XX:+UseBiasedLocking라는 옵션을 통해서 biased locking이라는 기능을 제공한다. ㄱ 전까지는 대부분의 객체들이 하나의 스레드에 의해 잠기게 되었지만 이 옵션을 켜면 스레드가 자기자신을향하여 bias된다. 즉, 이 상태가 되면 스레드는 많은 비용이 드는 인스트럭션 재배열 작업 통해서 잠김과 풀림 작업을 수행할 수 있게된다. 이 작업들은 진보된 적응 스피닝(adaptive spinning) 기술을 사용하여 처리량을 개선시킬 수 있다. 결과적으로 동기화 성능은 보다 빨라졌다..
HotSpotVM에서 대부분 도기화 작업은 fast-path 코드 작업을 통해서 진행한다. 만약 여러스레드가 경합을 일으키는 상황이 오면 이 fast-path 코드는 slow-path 코드 상태로 변환된다. 참고로 slow-path 구현은 C++이다. fast-path 코드는 JT 컴파일러에서 제공하는 장비에 의존적인 코드로 작성되어있다.
더 자세히 알고싶다면 'Java COncurrency in Practice'라는 책을 참고. 번역서 존재 '멀티코어를 100퍼세느 활용하는 자바 병렬 프로그래밍'
I/O에서 발생하는 병목 현상
기존 I/O를 사용하면 성능에 안좋다. NIO로 개선해보자! NIO(Non Blocking IO? new IO !!) Java 1.4부터 새로 나왔다 한다. 이걸 사용하면 자바에서도 비교적 빠른 IO를 기대할 수 있다
자바에서 입력과 출력은 스트림을 통해서 이루어진다. 스트림은 데이터에 따라 비트, 바이트 단위로 다뤄질 수 있으며 파이프라인과 같이 선입선출의 순차적으로 처리됩니다.
파일 IO만 해당되는 것이 아니라 어떤 디바이스를 통해 이뤄지는 작업을 모두 IO라고 한다. 네트워크를 통해서 다른 서버로 데이터를 전송하거나, 다른 서버로 부터 데이터를 전송 받는 것도 IO이다. 데이터의 입출력을 전반적으로 의미.
System.out.println("Nya");
out은 PrintStream을 System 클래스에 static으로 정의해놓은 변수이다.
IO는 성능에 영향을 가장 많이 미친다. IO에서 발생하는 시간은 CPU를 사용하는 시간과 대기 시간 중 대기 시간에 속하기 때문이다. IO와 관련된 디바이스가 느릴 수록 애플리케이션의 속도가 느려진다.
자바에서 파일을 읽고 처리하는 방법은 굉장히 많다. java.io 소속의 스트림 클래스가 있다.
스트림을 읽는 데 관련된 주요 클래스는 다음과 같다( 스트림을 쓰는데 관련된 클래스들은 Input을 Output으로 바꾸어 사용하면 된다). 여기에 명시된 모든 입력과 관련된 스트림들은 java.io.InputStream 클래스로부터 상속 받았다. 바이트 기반의 스트림 입력을 ㅓ리하기 위해서는 이 클래스의 하위 클래스를 사용한다.
- ByteArrayInputStream: 바이트로 구성된 배열을 읽어서 입력 스트림을 만든다.
- FIleInputStream: 이미지와 같은 바이너리 기반의 파일의 스트림을 만든다
- FilterInputStream: 여러 종류의 유용한 입력 스트림의 추상 클래스이다.
- ObjectInputStream: ObjectOutputStream을 통해서 저장해 놓은 객체를 읽기 위한 스트림을 만든다. (Serializable)
- PipedInputStream: PipedOutputStream을 통해서 출력된 스트림을 읽어서 처리하기 위한 스트림을 만든다.
- SequenceInputStream: 별개인 두 개의 스트림을 하나의 스트림으로 만든다.
문자열 기반의 스트림을 읽기 위해서 사용하는 클래스는 이와는 다르게 java.ioReader 클래스의 하위 클래스들이다.
- BufferedReader: 문자열 입력 스트림을 버퍼에 담아서 처리한다. 일반적으로 문자열 기반의 파일을 읽을 때 가장 많이 사용한다.
- CharArrayReader: char의 배열로 된 문자 배열을 처리한다.
- FilterReader: 문자열 기반의 스트림을 처리하기 위한 추상 클래스이다.
- FileReader: 문자열 기반의 파일을 읽기 위한 클래스이다.
- InputStreamReader: 바이트 기반의 스트림을 문자열 기반의 스트림으로 연결하는 역할을 한다.
- PipedReader: 파이프 스트림을 읽는다.
- StringReader: 문자열 기반의 소스를 읽는다.
바이트 단위로 읽거나, 문자열 단위로 읽을 때 중요한 것은 한 번 연 스트림은 반드시 닫아 주어야 한다는 것이다. 스트림을 닫지 않으면 나중에 리소스가 부족해질 수 있다. 예를 들어 파일을 열지 못하는 경우가 발생하면, 관련 파일을 관리하는 스트림의 상태 변경이 불가능해지기 때문이다.
FileReader 같은걸로 파일 처리하면 시간 많이 걸리는데 문자열을 하나씩 처리하도록 되어있어서 그렇다.
버퍼를 쓰자. 특정 배열에 읽은 데이터를 저장한 후 그 데이터를 사용하면, 더 빠르게 처리할 수 있다.
read에 매개변수를 담아 보내면 파일에서 읽은 char 배열의 개수가 리턴된다. char 배열을 버퍼로 사용하여서 마지막 char들이 배열의 크기와 딱 맞지 않으면 쓰레기값이 들어갈 수 있다. 그렇기에 read에서 반환될 파일에서 읽은 char 배열의 개수만큼만 저장한다.
이러한 간단한 FileReader를 사용하진 않는다. 문자열 단위로 읽는 것은 비효율적이기 때문이다. 만약 파일의 크기가 크면 OutOfMemoryError가 발생하게 된다. 따라서 파일의 크기가 크거나 반복 회수가 많을 경우에 대응하는 로직을 포함해야한다. 문자열 단위로 읽는 방식에 대한 해결 방법에는 BufferedReader 클래스가 있다.
BufferedReader 클래스는 다른 FileReader 클래스와 마찬가지로 문자열 단위나 문자열 배열 단위로 읽을 수 있는 기능을 제공한다. 추가로 라인 단위로 읽을 수 있는 ReadLine() 메서드를 제공한다. 문자열 읽는 거보다 간단해진다.
IO 병목 발생한 사례
사용자의 요청이 발생할 때마다 매번 파일을 읽도록 되어 있는 시스템이 있다.
경로를 하나 가져오기 위해 매번 config.xml 파일을 읽고 파싱하여 관련 DB 쿼리 데이터를 읽는다. 요청이 올때마다 파일에서 쿼리를 읽어야 하니 엄청난 IO 처리량 발생.
웹서비스에선 DB 쿼리나 여러 종류의 설정을 파일에 저장하고 사용하는 경우가 많다. 해당 파일에 수정사항이 있는지 확인하기 위해 매번 ㅏ일을 불러와 File 객체를 만들어 검사하면 매번 비용이 든다. 이러한 부분을 해결하기 위해선 데몬(Daemon) 스레드를 하나 생성하여 5분이나 10분에 한 번씩 확인하도록 수정하는 것이다.
NIO의 원리는 어떻게 되는 걸까..
올드한 IO 클래스는 두고 JDK 1.4부터 추가된 NIO가 어떤 것인지 알아보자. NIO가 무엇인지 자세히 할기 위해서 근본적으로 IO작업이 운영체제에서 어떻게 수행되었는지를 알아야한다. 만약 자바를 사용해 하드 디스크에 있는 데이터를 읽는다면 어떤 프로세스로 진행이 될까?
1) 파일을 읽으라는 메서드를 자바에 전달한다.
2) 파일명을 전달받은 메서드가 운영체제의 커널에게 파일을 읽어 달라고 요청한다.
3) 커널이 하드 디스크로부터 파일을 읽어서 자신의 커널에 있는 버퍼에 복사하는 작업을 수행한다. DMA에서 이 작업을 하게 된다. Direct Memory Access로 CPU의 개입없이 주변 장치와 메모리 사이에 통신, 전달을 한다.
4) 자바에서는 마음대로 커널의 버퍼를 사용하지 못하므로, JVM으로 그 데이터를 전달한다.
5) JVM에서 메서드에 있는 스트림 관리 클래스를 사용하여 데이터를 처리한다.
자바에서는 3번 복사 작업을 할 때에나 4번 전달 작업을 수행할 때 대기하는 시간이 발생할 수 밖에 없다. 이러한 단점을 보완하기 위해서 NIO가 탄생했다. 3번 작업을 자바에서 직접 통제하여 시간을 더 단축할 수 있게 한 것이다. NIO를 사용한다고 IO에서 발생하는 모든 병목 현상이 해결되는 것은 아니지만, IO를 위한 여러 가지 새로운 개념이 도입되었다.
- 버퍼의 도입
- 채널의 도입
- 문자열의 엔코더와 디코더 제공
- Perl 스타일의 정규 표현식에 기초한 패턴 매칭 방법 제공
- 파일을 잠그거나 메모리 매핑이 가능한 파일 인터페이스 제공
- 서버를 위한 복합적인 Non-blocking IO 제공
자세한 건 오라클에서 확인하자 참 알찬 내용이라 한다.
DirectByteBuffer를 잘못 사용하여 발생한 문제
NIO를 사용할 때 ByteBuffer를 사용하는 경우가 있다. ByteBuffer는 네트워크나 파일에 있는 데이터를 읽어 들일 때 사용한다. ByteBuffer는 네트워크나 파일에 있는 데이터를 읽어 들일 때 사용한다. ByteBuffer 객체를 생성하는 메서드에는 wrap(), allocate(), allocateDirect()가 있다. 이 중에서 allocateDirect() 메서드는 데이터를 자바 JVM에 올려서 사용하는 것이 아니라, OS 메모리에 할당된 메모리를 Native한 JNI로 처리하는 DirectByteBuffer 객체를 생성한다. 그런데, 이 DirectByteBuffer 객체는 필요할 때 계속 생성해서는안된다.
ByteBuffer 클래스의 allocateDirect() 메서드를 호출함으로 DirectByteBuffer 객체를 생성한 후 리턴하나. GC 상황을 모니터링 하게되면
5, 10초에 한 번 씩 Full GC가 발생한다. Old 영역의 메모리는 증가하지 않고. 그 이유는 DirectByteBuffer의 생성자 때문이다. 이 생성자는 java.nio에 아무런 접근 제어자가 없이 선언된 (package private인) Bits라는 클래스의 reserveMemory() 메서드를 호출한다. 이 reserveMemory()는 JVM에 할당되어 있는 메모리 보다 더 많은 메모리를 요구할 경우 System.gc() 메서드를 호출하도록 되어 있다. JVM에 있는 코드에 System.gc()메서드가 있기 때문에 해당 생성자가 무차별적으로 생성될 경우 GC가 자주 발생하고 성능에 영향을 준다. 그래서 DirectByteBuffer를 생성할 땐 신중하여야 한다. 가능하면 singleton 패턴을 사용하여 해당JVM에는 하나의 객체만 생성하도록 하는 것을 권장.
lastModified() 메서드의 성능 저하
jdk 6까지는 자바에서 파일이 변경되었는지를 확인하기 위해 FIle 클래스에 있는 lastModified()라는 메서드를 사용했는데. 이건 최종 수정된 시간으 밀리초 단위로 제공한다. 얘 처리되는 절차가 복잡하다.
1) System.getSecuritymanager() 메서드를 호출하여 Securitymanager 객체 얻어옴
2) 만약 null이 아니면 Securitymanager 객체의 checkRead() 메서드 수행
3) File 클래스 내부에 있는 FileSystem 이라는 클래스의 객체에 getLastModifiedTime() 메서드를 수행하여 결과 리턴.
보기엔 3단계이지만, 각 호출되는 메서드에서 호출되는 메서드들이 많다.
jdk 7부터는 새로운 개념의 IO 처리를 한다. 성능에 영향을 주는 WatcherService 존재.
1) Path 객체를 생성해서 모니터링할 디렉터리를 지정한다.
2) WatchService 클래스의 Watcher라는 객체를 생성한다.
3) dir이라는 Path 객체의 register라는 메서드를 활용하여 파일이 생성, 수정, 삭제되는 이벤트를 처리하도록 지정하였다.
4) Watcher 객체의 take() 메서드를 호출하면 해당 디렉터리에 변경이 있을 때 까지 기다리다, 작업이 발견되면 key라는 WatchKey 클래스의 객체가 생성된다. Socket 관련 객체에 accept() 메서드처럼 기다린다.
5) 파일에 변화가 새겼다면 이벤트의 목록을 가져온다.
6) 이벤트를 처리한 다음 key 객체를 리셋한다.
변화가 있는지 지켜보는 방식이 궁금하네 어떻게 구현한건지. 지켜본느데에도 데몬같은게 사용된거 아닌가. 데몬 같은게 어떻게 IO 호출없이 쳐다볼 수 있는거 처럼 말을 하는거지...
NIO 1, 2에 대해서는 따로 공부해서 적어봐야겠다.
로그는 반드시 필요한 내용만 찍자.
오느로거를 쓰던 성능 이슈 발생 가능
콘솔에 출력하는 로그가 있고 파일에 저장 시키는 듯한 로그가 있다 화면에 출력할 때 커널 CPU를 많이 점유한다.
개선율 튜닝 전 후 차이를 수치화
(튜닝 전 응답 속도 - 튜닝 후 응답 속도) x 100 / 튜닝 후 응답 속도 = 개선율 (%)
이러한 문제의 원인에는 로그를 남길 때 완전히 처리될 때까지 다음 작업은 대기ㅣ해야한다. 그리고 실제 운영할 때 콘솔출력은 볼 수 없다
JDK 5.0 PrintStream에 새로 추가된 메서드 format 있다. System.out.format().. 실제 사용 될 때 컴파일을 하게되면 새로운 Object 배열을 생성해 그 값을 배열에 포함시키게 된다. 그래서 long값을 Object 형태로 나타낸다. String.format() 메서드 내부에 java.util.formatter 클래스를 호출한다.
얜 %가 들어가 있는 forat 문자열을 항상 파싱하기에 문자열을 그냥 더하는 것보다 성능 별로야.
그래도 개발 디버깅할 땐 가독성 좋은 format 쓰자..
어떻게 해야 컴ㅍ아일할 때 시스템 로그가 삭제될 수 있을까.
반드시 false인 if로 감싸면 컴파일 시 제거된다.
log4j, slf4j, logback 같은거 쓰자.디버깅 상황에 쓸 로그, 심각한 상황에 쓸 로그 등을 정하고 현재 프로그램의 로깅 수준을 정해서 처리할 수 있다. 거추장스럽게 if를 쓸 필요도없다.
스택트레이스 좀 깔끔하게 보자
try {
} catch (Exceptioin e) {
StackTraceElement[] ste = e.getStackTrace();
String className = ste[0].getClassName();
String methodName = ste[0].getMethodName();
int lineNumber = ste[0].getLineNumber();
Strin fileName = ste[0].getFileName();
logger.severe("Exception : "+e.getMessage());
logger.severe(className+"."+methodName+" "+fileName+" "+lineNumber+"line");
필요없는 디버깅 로그는 지우자
JSP와 서블릿, Spring에서 발생할 수 있는 여러 문제점
WAS에서 병목현상이 발생할 수 있는 부분이라면, UI 부분과 비즈니스 로직 부분으로 나눌 수 있다. UI란 서버에서 수행되는 UI를 이야기하는 것. 자바 기반의 서버단 UI를 구성하는 대부분의 기술은 JSP와 서블릿을 확장하는 기술이다. 그만큼 JSP와 서블릿의 기본에 잘 알아야한다.
JSP와 Servlet의 기본적인 동작 원리.
JSP와같은 웹 화면단을 처리하는 부분에서 소요되는 시간은 많지 않다. JSP의 경우 가장 처음 호출되는 경우에만 시간이 소요되고, 그 이후의 시간에는 컴파일된 서블릿 클래스가 수행되기 때문이다. 그럼 JSP의 라이프 사이클을 간단히 보자
1) JSP URL 호출
2) 페이지 번역
3) JSP 페이지 컴파일
4) 클래스 로드
5) 인스턴스 생성
6) jspInit 메서드 호출
7) _jspService 메서드 호출
8) jspDestroy 메서드 호출
여기서 jSP 페이지가 이미 컴파일이 되어 있고 클래스가 로드되어있고 jSP 파일이 변경되지 않았다면, 가장 많은 시간이 소요되는 2~ 4프로세스는 새약된다. 서버의 종류에 따라 서버가 기동될 때 컴파일을 미리 수행하는 Pre compile 옵션이 있다. 이 옵션을 선택하면 서버에 최신 버전을 반영한 이후 처음 호출되었을 때 응답 시간이 느린 현상을 방지할 수 있다. 개발 시 사용하면 좀 귀찮다. 컴파일할때마다 느릴테니
서블릿의 라이프 사이클을 보자, WAS의 JVM이 시작한 후,
- Servlet 객체가 자동으로 생성되고 초기화 되거나,
- 사용자가 해당 Servlet을 처음으로 호출했을 때 생성되고 초기화 된다.
- 계속 '사용 가능' 상태로 대기한다. 중간에 예외 발생 시 '사용 불가능' 상태로 빠졌다가 다시 '사용 가능' 상태로 변환
- 더 이상필요없으면 '파기' 이후 JVM에서 제거
생성. 성공 시 초기화, 실패 시 제거
초기화. 성공 시 사용 가능, 실패 시 제거
사용 가능, 불가능 스위치
이후 destroy request로 파기
파기 성공 후 제거
기억할 점은 서블릿은 jVM에 여러 객체로 생성되지 않는다. WAS가 시작하고 '사용 가능' 상태가 된 이상 대부분의 서블릿은 JVM에 살아있고, 여러 스레드에서 해당 서블릿의 serice() 메서드를 호출해 공유한다. 서블릿 클래스의 메서드 내에 선언한 지역 변수가 아닌 멤버 변수를 선언하여 service()메서드에서 사용하면 어떤 일이 일어날까.
즉 스태틱 사용하는 것과 같이 여러 스레드에서 접근해 값이 바뀔 수 있다.final로 쓰면 어떨까나 상황에 따라. service() 메서드 구현 시 멤버 변수나 static한 클래스를 변수를 선언해 지속적으로 변경하는 작업은 피해라.
적절한 include 사용.
다른 JSP와 혼합해서 하나로 만들기 가능.
정적인 방식, 동적인 방식있다.
정적인 방식은 jSP 페이지 번역 및 컴파일 단계에서 필요한 JSP 읽어 포함 시킨다.
동적인 방식은 호출될 때마다 불러와 수행하도록 한다.
정적인 방식: <%@ include ...
동적인 방식: <jsp:inclue page= ...
동적인 방식이 더 느리다. 성능을 빠르게 하려면 정적인 걸로하자.
정적인 방식을 쓰면 메인 JSP에 추가되는 JSP가 생긴데 동일 이름 변수 있음 오류 생길 수 있으니 잘 보고 해야한다.
자바 빈즈는 UI에서 서버 측 데이터를 담아 처리하기 위한 컴포넌트.
useBean을 하면 성능에 많은 영향을 미치진 않아도, 너무 사용하면 JSP에서 소요되는 시간 증가
이런걸 줄이기 위해선 Transfer object 패턴을 써야한다. TO 클래스를 만들고 사용된 각 문자열 및 해쉬맵, 리스트를 그 클래스의 변수로 지정하여 사용하면 좋다. 한 두 개의 자바 빈즈면 몰라도 10~20개의 자바빈즈느 영향을 준다.
태그 라이브러리도 잘 써야한다. JSP에서 공통적으로 반복되는 코드를 클래스로 만들고 HTML 태그와 같이 정의된 ㅐ그로 쓸 수 있게 하는 라이브러리.
태그라이브러리 코드를 잘 못 작성하거나, 전송되는 데이터가 많을 때 성능에 문제가 된다. 목록을 처리하면서 대용량의 데이터를 처리할 경우, 태그 라이브러리의 사용을 자제.
스프링 프레임워크 간단 정리
스프링의 가장 큰 특징은 복잡한 앺ㄹ리케이션도 POJO(Plain Old Java Object)로 개발 가능.
Servlet은 POJO가 아니다. Servlet을 개발하려면 반드시 HttpServlet이라는 클래스를 상속해야한다. 하지만 스프링을 사용하면 확장하지 않아도 웹 요청을 리하 수 있는 클래스를 만들 수 있다.
스프링 핵심 기술
DI 의존성을 외부로부터 주입 받아, 객체를 교체하기도 쉽고 의존성이 낮아진다.
AOP 비즈니스 로직에 관련 없는데도 비슷한 코드가 여러 클래스에서 중복되는 경우 따로 뺴내버린다.
PSA Portable Service Abstraction: 사용하는 기술이 바뀌더라도비즈니스 로직에는 변화가 없도록 추상화 계층을 두는 것
발생할 수 있는 문제점
빈 설정을 잘못해서, 동작 원리를 이해하지 못해서 등 여러가지 있다. 성능과 관련된 문제만 보자.
성능과 가장 관련 많은 부분은 프록시 이다. 스프링 프록시는 기본적으로 실행 시에 생성된다. 개발할 때 적은 요청을 할 때는 문제가 없지만, 요청량이 많은 운영 상황으로 넘어가면 문제가 나타날 수 있다. 스프링이 프록시를 사용하게 하는 주요 기능은 바로 트랜잭션이다. @Transactional 어노테이션을 쓰면 해당 ㅓ노테이션을 사용한 클래스의 인스턴스를 처음 만들 때 프록시 객체를 만든다. 이 밖에도 개발자가 직접 스프링 AOP를 사용해서별도의 기능을 ㅜ가하는 경우에도 프록시를 사용하는데,이 부분에서 문제가 많이 발생한다. @Transactional 같은 경우엔 오랜시간 동안 상당한 테스트를 거치고 많은 사용자에게 검정 받았지만 개인의 것은 그렇지 않다. 간단한 부하 툴을 사용해서라도 성능적인 면을 테스트 해야한다. 추가로 ㅅ프링이 내부 매커니즘에서 사용하는 캐시도 조심해서 써야한다.
스프링이 제공하는 ViewResolver 중에 자주 사용되는 InternalResourceViewResolver에는 그러한 캐시 기능이 있다. 만약 매번 다른 문자열이 생성될 가능성이 높고, 상당히 많은 수의 키 값으로 캐시값이 생성될 여지가 있는 상황에서는 문자열을 반환하는 게 메모리에 치명적일 수 있다. 이러한 상황엔 뷰 이름을 문자열로 반환하기 보단 뷰 객체 자체를 반환하는 방ㅂ법이 메모리 릭을 방지하는데 도움이 된다.
return new Redirectview()....
DB를 사용하면서 발생 가능한문제점들
자바 기반 애플리케이션ㅅ ㅓㅇㄴㅇ 보면 앱 응답시간을 지연시키는 대부분 요인은 DB 쿼리 수행 시간과 결과를 처리하는 시간. DB 관련 부분에서 잘못 처리하는 사례들이 많으므로 , 그 부분에서의 실수를 최소화하기 위해서 관련된 내용에 대해서 알아보자.
- DB connection을 할 경우 반드시 공통 유틸을 사용할 것
- 각 모듈별 DataSource를 사용하여 리소스가 부족한 현상이 발생하지 않도록 할 것.
- 반드시 Connection, Statement 관련 객체, ResultSet을 close할 것
- 페이지 처리를 하기 위해서 ResultSet객체.last() 메서드를 사용하지 말 것.
JDBC 관련 API는 클래스가 아니라 인터페이스이다. JDK의 API에 있는 java.sql 인터페이스를 각 DB 벤더에서 상황에 맞게 구현하게 한다.
1) 드라이버를 로드한다.
2) DB 서버의 IP와 ID, PW 등을 DriverManager 클래스의 getConnection 메서드를 사용하여 COnnecetion 객체로 만든다.
3) Connection으로부터 PreparedStatement 객체를 받는다.
4) executeQuery를 수행하여 그 결과로 ResultSet 객체를 받아 데이터를 처리한다.
5) 모든 데이터를 처리한 이후에는 finally 구문을 사용하여 ReesultSet, Prepared Statement, Connection 객체들을 닫는다. 객체를 close할 떄도 예외가 발생할 수 있으니 예외를 던지도록 처리한다.
JDBC 관련 클래스에 대해서 기본 개념과유념해야 할 부분이 있다.
가장 느린 부분은 COnnection 객체를 얻는 부분이다. 같은 장비에 DB가 구성되어 있다하더라도, DB와 WAS 사이에 통신을 해야한다. 다른 장비에 있다면 더 소요되고갑자기 증가하면 Connection 객체 얻기 위한 시간 소모 더 커짐.
객체 생성 발생 대기 시간 줄이고, 네트워크의 부담을 줄이기 위해 하는게 DB Connection Pool.
JSP와 서브릿 기술이나오면서 커넥션 풀에 대한 소스코드들도 많이 나옸따. 그러나 문제점도 같이 발견. WAS에서 커넥션 풀을 제공하고, DAtaSource를 사용하여 JNDI로 호출해 쓸 수 있기 때문에 이 부분에서 발생하던 문제는 많이 줄었다. 아직 WAS 에서 제공하는 DB Connection Pool을 사요하지 않고 출저 불분명한 Connection Pool을 사용하는 사이트가 있다. 검증된 DB Connection Pool, DataSource 쓰자 이 둘의 차이는 DataSource는 JDK 1.4부터 생긴 표준. Connecetion Pool로 연결을 관리해야 하고,트랜잭션 관리도 가능하도록 만들어야 한다. 그러므로 DataSource가 DB Connection Pool을 포함한다고 생각해두면 된다. 여기서 유의할 점은 자바표준이 없어 벤더에 따라 사용법이다를 수 있어. 그래도 표준이니 사용법은 동일하다 DateSource
https://free-strings.blogspot.com/2016/04/adapter-decorator-facade-proxy.html
https://scshim.tistory.com/451