JPA를 동작시키기 위한 내부 객체들이 이 Factory를 통해 생성되고, 구현체에 따라선 커낵션 풀도 별도로 구성하므로 생성 비용이 크다고 한다.
여러 EntityManagerFactory를 생성하여 공유할 수 있으나. 하나의 객체만 생성해서 전 영역에 공유하는 것이 권장된다. (configuration lite mode를 사용하면 여러 개를 생성하고 공유할 수 있을 것 같다.)
EntityManager (Persistence Context)
엔티티 정보를 저장하고 있는 환경. 요청 결과가 DB에 반영되기 이전에 위치하는 논리적인 영역을 의미한다. 가상의 데이터베이스 영역이라고 봐도 무방하다.
사용자의 요청을 받아 Connection을 통해 DB와 로컬 트랜잭션을 맺으며, 데이터를 관리하고 영속적으로 반영하는 혹은 삭제하는 책임을 가지는 객체이다.
JPA의 대부분의 기능은 해당 객체가 지원한다. 커넥션과 밀접한 관계를 가지기에 스레드 간의 공유, 재사용이 어렵다.
멀티스레드에 안전하지 않은 상태가 된다는 것 아닐까?
Entity의 Life Cycle
비영속 (new, transient) :영속성 컨텍스트와 관계가 없는 새로 생성된 상태
// 비영속 상태의 Entity. 즉 entityManager가 모르고 있는 Entity이다.
Entity entity = new Entity();
entity.setXxxA("test");
entity.setXxxB("value");
영속 (managed) : 영속성 컨텍스트에게 관리되는 상태 (환경 안에 정보가 있는 상태)
JPA는 기본적으로 트랜잭션 안에서 데이터를 변경하여야 한다.
// 요청에 따른 entityManager(Persistence Context) 생성
EntityManager entityManager = entityManagerFactory.createEntityManager();
// 영속성 컨택스트에 대한 글로벌 트랜잭션 관리 시작
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
try {
// 비영속 상태의 Entity.
Entity entity = new Entity();
entity.setXxxA("test");
entity.setXxxB("value");
// 영속성 컨택스트에 Entity 정보를 저장. 즉 entityManager가 알고있는 Entity이다.
entityManager.persist(entity);
// transcation 결과 반영 - (DB 저장 X, 영속성 컨텍스트에만 반영)
transaction.commit();
} catch (RuntimeException exception) {
// 위 로직 실패 시 결과 반영 X -> 원복
transaction.rollback();
} finally {
// 성공, 실패 상관없이 transaction 종료
transaction.close();
}
준영속 (detached) : 영속성 컨텍스트에 관리되다가 이후 분리된 상태
영속성 컨텍스트가 지원하는 기능을 사용할 수 없기에 비영속 상태와 유사하다. 다른 점은 식별자 값(id)이 존재한다는 점이다.
// 특정 회원 엔티티를 영속성 컨텍스트에서 분리한다.
// 쉽게 말하면 해당 엔티티를 영속성 컨텍스트에 존재하는 1차 캐시에서 삭제한다.
entityManager.detach(entity);
// 해당 영속성 컨텍스트에서 관리되는 모든 엔티티를 준영속화한다.
// 쉽게 말하면 영속성 컨텍스트의 1차 캐시를 초기화한다.
emtityManager.clear();
// 영속성 컨택스트를 종료시킨다.
entityManager.close();
삭제 (removed) : 엔티티가 삭제된 상태
// 해당 객체를 삭제. 즉 DB에 저장된 정보를 삭제하는 요청을 전송한다.
entityManager.remove(entity);
준영속과 삭제의 다른 점은 Entity가 DB에 반영되기 전이냐 이후이냐의 차이이다.
JPA Internal Flow
Persistence Context의 특징
영속성 컨텍스트와 식별자 값
영속성 컨텍스트는 @ID로 매핑된 값을 통해 엔티티를 구분한다. 즉 해당 값이 무조건 존재해야 하며, 없는 경우 예외가 발생한다.
데이터베이스 저장 시점
영속성 컨텍스트에 저장, 관리되는 엔티티는 flush()가 호출된 시점에 실제 DB에 반영된다.
엔티티 관리 방식의 장점 -아래
JPA 엔티티 관리 방식의 장점
1차 캐시
영속성 컨텍스트에서 관리되는 데이터를 우선적으로 조회하는 것을 의미한다.
사용자의 요청이 종료된다면 해당 영역도 제거되기에 하나의 비즈니스 로직에서만 이점을 제공한다.
전역적으로 관리되는 캐시는 2차 캐시라고 부른다.
해당 정보가 컨텍스트 안에 존재하지 않는다면,
DB에서 해당 정보를 조회한 후 (SELECT)
쿼리 결과를 이용하여 엔티티를 생성한 다음
컨텍스트 내부에 엔티티를 저장하고 반환한다.
1차 캐시는 내부적으로 Map을 통해 관리되며, @Id로 매핑된 값을 Key로 가진다. 이 값은 DB의 PK 값인 경우도 존재한다.*
동일성 보장
1차 캐시를 이용하여 반복 가능한 읽기 등급의 격리 수준을 Application 수준에서 제공한다. 즉 같은 트랜잭션 내에 여러 번 조회 쿼리를 진행하더라도 동일한 결과를 반환받는 것을 이야기한다.
이때에는 동일성 비교가 가능하다. → Map에서 동일한 엔티티를 제공한다.
트랜잭션을 지원하는 쓰기 지연
한 트랜잭션 내에서 여러 엔티티를 등록할 때 각각의 엔티티에 대한 Insert Query를 DB에 바로 요청하지 않고, 일종의 버퍼처럼 컨택스트 내부에 존재하는 SQL 저장소에 Query를 쌓아놓다가 1번에 반영할 수 있다.
entityManager.commit();
큰 오버헤드를 가지는 i/o 처리를 최소화할 수 있다.
변경 감지
영속성 컨텍스트에게 관리되고 있는 엔티티 정보에 대해서 Service logic에서 setter 등을 통해 수정을 가할 경우, 그 변경 결과가 트랜잭션을 커밋하는 시점에 영속성 컨텍스트에게 감지되고 최종적으로 Update Query가 DB로 전송되어 최신 정보로 변경하게 된다.
엔티티 정보와 엔티티 스냅샷 정보를 비교하여 감지한다.
스냅샷 정보는 DB에서 값을 읽어와 캐시에 저장한 시점의 엔티티 정보를 복사하여 저장해둔 것이다. 이것을 트랜잭션이 커밋된 시점에서 엔티티 정보와 비교하여 변경된 사항들에 대해서 Update Query를 생성하는 것이다.
엔티티 삭제도 동일 메커니즘을 공유하고 트랜잭션 커밋 시 Delete Query를 전송한다.
Flush?
영속성 컨텍스트의 변경 내용들을 데이터베이스에 반영시키는 기능이다.
영속성 컨텍스트에 저장된 정보를 비우는 것은 아니다.
Flush 발생 시 Flow는
엔티티에 대한 변경을 감지하고
추가, 수정, 삭제된 엔티티 정보에 대한 Query를 내부 SQL 저장소에 등록한 뒤
해당 정보를 DB에 전송한다.
이렇게 이루어진다.
Flush를 발생시키는 방법으로는
em.flush()를 통한 직접 호출
트랜잭션 commit() 을 통한 자동 호출
JPQL으로 만들어진 Query 실행 시 자동 호출
영속성 컨텍스트에만 관리되고 있는 영속 상태의 엔티티들은 사실상 DB에 존재하지 않으므로 JPQL을 통한 Query 요청 시 결과에서 이 것들을 확인할 수 없게 된다. 이를 방지하기 위해 미리 Flush()를 호출하여 DB에 반영한 뒤 정상적인 결과를 제공하게 된다.
Application(JVM) 외부의 입력을 받고, 내부의 결과를 외부로 전달하는 모든 흐름을 의미한다.
Scanner, InputStream을 통한 키보드, CSV 등의 포맷 파일 입력 등
OutputStream을 통한 Console 출력, TCP/IP를 이용한 외부 Application(JVM)에 전달 등
Stream - Java I/O?
데이터를 전달하고, 입력받을 때 필요한 단방향 통로를 말한다. 기본적인 Java I/O API에서는 입력과 출력을 위한 각각의 Stream이 필요하다.
기본적으로 Buffer를 사용하지 않는 방식이며, 데이터를 받은 즉시 처리한다.
데이터를 순차적으로 처리한다. (FIFO : 들어간 순서대로 나온다.)
Blocking 방식으로 동작한다. 스트림 호출 시 동작 스레드는 대기 상태가 된다.
시스템 콜과 CPU 자원 사용, 커널에서 Direct Buffer → 다시 JVM 내부 Buffer로 메모리를 복사하는 과정이 존재하는 등 큰 오버헤드가 존재한다.
Buffer?
일반적으로 데이터가 한 위치에서 다른 위치로 이동하는 동안 일시적으로 데이터를 보유하는 데이터 구조 또는 메모리 영역을 말한다.
각각의 데이터가 큰 경우에 해당 영역에 버퍼 크기만큼의 데이터를 저장하고 전송함으로써 디스크와의 상호 작용 (호출) 수를 줄일 수 있다. - I/O 비용을 최소화
데이터를 받는 순서대로 쌓아 보관하다 일정량이 되면 한번에 전송한다.
데이터를 실시간으로 처리할 필요가 없고, 지속적으로 데이터를 넘긴다면 사용하는 것이 좋다.
서버가 요청을 처리하는 시간이 받는 요청을 따라가지 못한다면, 요청이 유실될 수 있다. → 이때 요청들을 담아둘 버퍼를 이용할 수 있다. server socket 구현체에는 buffer가 포함되어 있다.
Channel - Java N I/O?
데이터가 양방향으로 전송될 수 있는 통로를 말하며, Java 4, 7에 추가된 N I/O 1, 2 API는 기본적으로 Channel과 Buffer 그리고 Selector를 통하여 기능을 제공한다.
셀럭터에는 채널들을 등록하고, 사용 가능한 채널을 통해서 각각의 데이터를 전송하거나 받고 이를 버퍼에 보관한다. Application은 버퍼를 통해 데이터를 처리한다.
"Non-blocking 방식으로도" 동작 가능하다. files API의 기능들은 Blocking으로 동작한다.
Blocking 방식으로 동작하는 API에 대해서 인터럽트를 던짐으로써 대기 상태에서 빠져나올 수 있다.
기존 I/O 방식에서 접근할 수 없었던 Direct Buffer를 직접 참조할 수 있다. → 버퍼 내용을 복사할 필요가 없다.
InputStream과 OutputStream
바이트 기반 입출력 스트림들의 최상위 추상 클래스들이 InputStream, OutputStream이다.
InputStream?
입력과 관련된 API와 Closeable 인터페이스가 구현되어 있다.
바이트 기반의 입력 스트림을 구현하는데 사용되는 클래스이다.
하위 구현체로는
FileInputStream : 파일을 읽는 데 사용하는 구현체 - 텍스트, 바이트 코드 등을 읽을 때 사용한다.
FilterInputStream : InputStream을 재정의한 구현체 - 다른 입력 스트림을 포괄한다.
ObjectInputStream : ObjectOutputStream으로 저장된 데이터를 읽는 데 사용한다. → 역직렬화
등이 있다.
OutPutStream?
출력과 관련된 API와 Closeable 인터페이스가 구현되어 있다.
바이트 기반의 출력 스트림을 구현하는데 사용되는 클래스이다.
하위 구현체로는
FileOutputStream : 파일을 출력하는 데 사용하는 구현체
FilterOutputStream : OutputStream을 재정의한 구현체
ObjectOutputStream : Object를 Byte 기반으로 직렬 화할 때 사용한다.
등이 있다.
Byte와 Character 스트림
Byte Stream
기본적으로 사용되는 InputStream, OutputStream과 같이 1 바이트 단위를 처리하는 Stream들을 의미한다. 외부 요청을 서버가 처리하는 경우에는 이러한 데이터를 InputStreamReader를 통해 char로 변환하여 처리하게 되는데, 이 경우 한글 등으로 작성된 Unicode 기반의 정보들이 깨지는 문제가 발생하게 된다.
이를 위해 Character Stream이나 BufferdReader라는 것들이 만들어지게 되었다.
Character Stream
2 바이트 단위의 데이터를 처리하는 Stream들을 말하며, 기본적으로 Unicode charset에 맞게 데이터를 처리한다. 즉 Text 기반의 데이터를 입출력하는데 사용되는 Stream이다. JSON, HTML, XML...
보조 스트림
입출력 대상이 되는 파일이나 Binary 데이터를 직접 읽거나 쓰는 기능이 존재하지 않지만, 부가적인 기능을 추가하는 데 사용되는 Stream들을 말한다. Wrapper Stream이라고 하기도 한다.
이러한 구조를 가지는 구현 방식을 Decorator Pattern이라고 한다. 보조 스트림 중의 최상위는 FilterInputStream & FilterOutputStream이다.
하위 구현체로는
BufferedInputStream : 입력받는 바이트 스트림에 버퍼 기능을 제공하는 보조 스트림
BufferedOutputStream : 출력하는 바이트 스트림에 버퍼 기능을 제공하는 보조 스트림
BufferedReader : 문자 단위로 입력받는 스트림에 버퍼 기능을 제공하는 보조 스트림
BufferedWriter : 문자 단위로 출력하는 스트림에 버퍼 기능을 제공하는 보조 스트림
InputStreamReader : 입력되는 바이트 정보를 문자로 변환해주는 보조 스트림
OutputStreamReader : 출력되는 바이트 정보를 문자로 변환해주는 보조 스트림
DataInputStream : 입력되는 바이트를 자료형에 맞게 제공해주는 보조 스트림
DataOutputStream : 입력되는 바이트를 자료형에 맞게 출력해주는 보조 스트림
등이 있다.
표준 스트림 (System.in, System.out, System.err)
System.in
System.in 은 Static 한 InputStream 타입의 변수이다.
JVM이 메모리로 올라오면서 사용될 하위 구현체를 인스턴스화 하고, 기본적으로 키보드의 입력을 받을 때 사용한다.
바이트 단위로 동작한다.
System.out
System.out 은 Static 한 PrintStream 타입의 변수이다.
자체적인 Buffer를 가지고 있으며, print 요청을 모아두었다가 적절한 출력 시점에 flush()를 호출한다.
콘솔에 특정 문자열을 출력하는 데에 주로 사용된다. 로직 Logging을 위해서 등.. 하지만
GRASP : General Responsibility Assignment Software Patterns?
해당 내용은 상호작용하는 클래스 혹은 객체에 책임을 할당하는데 도움이 되는 개념과 방법 즉 패턴들로 이루어져 있습니다. 이러한 내용들을 준수함으로써 좋은 객체지향 디자인 패턴을 만들어낼 수 있습니다.
Responsibility?
책임은 SOLID나 지금 다룰 GRASP 등 Oriented-Object-Design 패턴과 원칙에서 핵심이 되는 개념입니다. 이는 클래스와 객체가 어떤 메시지(요청)에 대해 처리할 수 있거나, 적절한 행동을 해야 하는 의무가 있는 경우 해당 객체가 이러한 책임을 가진다라고 이야기할 수 있습니다.
책임은 메서드를 통해 구현되게 됩니다.
Example.
String은 문자열을 표현하고 처리하는 책임을 가집니다.
File은 파일에 대한 정보를 알려주고 처리하는 책임을 가집니다.
DTO는 데이터를 다른 Layer로 운반하고 특별한 경우 비즈니스 로직에 따라 데이터 값을 반영시키는 책임을 가질 수 있습니다.
이런 것이 모두 책임 입니다.
책임의 분류는 행위 관점과 상태 관점으로 나눌 수 있습니다.
행위 관점.
객체를 생성하는 행위, 결과를 계산하는 행위 - 객체 스스로 하는 것.
다른 객체의 행동을 지시하는 것 - Controller
다른 객체의 활동을 제어하고 조절하는 것 - Chain of Responsibility, Proxy
상태 관점.
private로 접근 제어된 데이터에 관하여 아는 것
관련된 객체에 대해 아는 것
자신이 유도하거나 계산할 수 있는 것에 관해 아는 것
Creator
새로운 객체를 생성하는 책임을 말합니다.
이러한 책임을 가지기 위해서는 하나 이상의 조건을 만족하여야 합니다.
생성하는 객체를 포함합니다.
생성하는 객체에 대한 초기화 정보를 가집니다.
생성하는 객체를 사용합니다.
생성하는 객체에 대한 정보를 기록합니다.
이와 관련된 것은 Creation Pattern 중 Abstract Factory, Factory Method가 있습니다.
Information Expert
새로운 기능이나 방법을 추가하는 것은 그에 필요한 정보를 가지는 클래스, 객체를 대상으로 하여야 합니다. 그러한 클래스, 객체는 최소한의 변경으로 기능을 구현할 수 있기 때문입니다.
결국 객체가 가지는 데이터에 대한 기능을 직접 처리하게끔 하라는 것입니다.
하지만 모든 필드에 대한 무분별한 Getter, Setter는 캡슐화를 지키지 못합니다.
해당 행위를 주관하는 객체(Client, Service)에서 데이터 흐름을 만들어내는 객체(DTO)의 데이터를 꺼내오는 것보다는 다음 행위를 하는 객체에게 해당 객체를 전달하는 것이 캡슐화를 지키는 방법입니다.
그리고 상태에 대한 질의 후 분기해야 하는 로직의 경우 Tell Don't Ask 원칙을 준수하는 것이 좋습니다.
요청에 대한 비즈니스 로직과 요청을 전달, 지시하는 로직은 분리되어야 합니다. 그리고 우리는 일반적으로 MVC Pattern을 통해 이러한 요구사항을 만족합니다.
Controller는 요청을 받고 적절한 행위를 하는 객체에게 지시하는 행위 관점의 패턴입니다.
이러한 책임을 가지는 객체는 내부적으로 별도의 비즈니스 로직을 가져서는 안 되며, 요청을 전달, 위임하는 것에 중점을 두어야 합니다.
Low Coupling
낮은 결합도를 준수하는 것은 시스템 설계의 전체적인 결합도를 낮게 유지되도록 구현하고 책임을 할당하라는 의미입니다. 객체 간의 결합이 없을 수는 없기에 우리는 최대한 영향을 주지 않도록 노력하여야 합니다.
이를 위해서는
클래스 간의 종속성을 낮추어야 합니다.
한 클래스의 변경이 다른 클래스에 주는 영향이 적어야 합니다.
객체는 더 높은 재사용 가능성을 가져야 합니다.
이는 Interface 등을 이용한 상호 작용이나 Facade 패턴 등을 사용하여 이루어낼 수 있습니다.
반대로 높은 결합도는 각각의 객체가 서로의 구현 세부사항을 인식할 때(접근 가능할 때, 사용할 때) 발생되게 되는 것입니다. 이런 경우 한 객체의 변경이 다른 객체의 변경을 야기하게 됩니다. extend의 경우 이런 변경에 대한 파급효과가 강해지게 됩니다.
High Cohesion
높은 응집력을 준수하라는 것은 객체 책임에 의해 변경될 수 있는 요소들을 한 곳에 모아서 관리하라는 것입니다.
이것을 통해 요구사항으로 인해 책임의 변경이 이루어진다면, 그러한 영향이 하나의 객체에 대해서만 이루어질 수 있습니다.
이것과 관련된 원칙은 SOLID의 Single responsibility principle가 있습니다. 이 원칙은 객체를 변경할 수 있는 책임이 하나만 존재해야 한다는 원칙입니다.
Indirection
간접이란 두 객체 간의 직접적인 결합을 분리하고 간접적인 결합을 지원하게끔 설계하라는 것입니다. 이는 별도의 중개자 객체를 통해 Dependency Injection을 진행하거나, 요청에 대한 위임, 지시하는 컨트롤러와 같은 요소를 도입하는 것을 말합니다.
Observer, Facade, Bridge, Mediator, Adapter는 간접 접근 방식을 활용하는 패턴입니다.
Polymorphism
하나의 타입을 가지는 서로 다른 객체들이 동일한 책임, 메시지에 대해서 서로 다르게 반응하는 것을 말합니다. 즉 책임을 공유하는 상태입니다.
이러한 원칙은 객체들 사이의 대체 가능성을 의미하며 그를 통해 설계를 유연하고 재사용 가능하게 만들 수 있습니다.
각각의 요청에 대하여 Reflection API 등의 동적 로딩을 통하여 Runtime Dependency를 구현하거나 Chain of Responsibility와 같은 행위 패턴을 구현하고 분기를 위한 상태 변수를 전달함으로써 컴파일 시점에서 필요한 객체를 Dependency Injection 할 수 있습니다.
Protected Variations
요구사항에 대한 책임, 동작 변화는 객체의 내부에서만 이루어져야 하며, 공용 인터페이스를 동일하게 유지함으로써 Client 역할을 하는 객체에게는 변경 사실을 숨겨야 합니다.
변경될 여지가 있는 로직에 대해서는 객체 간의 캡슐화를 지켜야 한다는 것입니다.
이와 관련된 원칙으로는 SOLID의 Open/closed principle이 있습니다. 이 원칙은 객체의 책임의 확장과 변경이 다른 객체의 행위에 영향을 주지 않아야 된다는 것을 말합니다.
Pure Fabrication
객체가 광범위한 책임을 가지지 않도록 (God Class) 분리하라는 것을 말합니다. 이는 기능에 대한 공통적인, 비즈니스 로직을 하나의 객체를 만들어 담당하게끔 하는 것을 뜻합니다.
Layer Architecture에서 이러한 책임을 담당하는 객체를 Service라고 합니다.
Runtime 시점에서 사용할 Instance를 선택하고 동작시킬 수 있는 유연성을 제공한다.
특정 객체를 감싸 추가적인 기능을 제공할 수 있다. RTW : JDK Dynamic Proxy
단점
Compile time에 Type, Exception 등의 검증을 진행할 수 없다. Runtime에서 가져오기때문
Runtime에서 Instance가 선택되기 때문에 해당 로직의 구체적인 동작 흐름을 파악하는 것에 대해 어려움을 가지게 된다.
Private 접근 제어자로 캡슐화된 필드, 메서드에 대해 접근 가능하기 때문에 기존 동작을 무시하고 깨트리는 행위가 가능해진다. Singleton 객체, Internal API 사용 등
Java 보안 관리자에게 Runtime 때 특정 권한을 지정받게 되는데, 이는 Linux의 Root 계정처럼 보안 취약점을 만들고, 제약 사항을 위반할 수 있다.
Reflection 성능 이슈?
"Java Reflection API가 느리고 높은 비용을 사용한다"라는 이야기는 흔히 듣게 되는 이야기이며, 이는 Reflection API의 Method Invoke() 실행 시간을 측정하는 ( 정적 메서드 디스패치와 비교하는 ) 많은 테스트들에서 나타나는 결과이다.
하지만 이는 Reflection만을 테스트하는 것이 아니라, 동적으로 Class를 Load 하고, Heap에 객체를 띄우는 선행 절차가 존재하기에 나타나는 결과이다.
그렇다고 Reflection API가 느리지 않고, 동일한 비용을 사용한다는 것은 아니다. 그러한 이유 중 하나로는 Reflection을 통한 초기 호출 시 JVM이 해당 정보를 미리 최적화할 수 없기 때문이다.
JIT Compiler의 Bytecode Caching, Opcode Optimization.. 등
즉 초기 호출 이후로는 캐싱을 통해서 Reflection API를 통한 메서드 호출도 최적화된다는 것을 의미한다.
초기 호출에서는 5배 이상의 차이를 보이더라도 이후 호출부터는 그러한 간격이 줄어들게 된다. 하지만 setAccessible과 같은 Class 정보 설정 기능을 사용하는 경우에는 그렇지 않을 수 있다.
2월 8일 추가
테스트 코드
public class ReflectionTest {
// get class name
// Reflection API 19~25ms
@Test
public void reflectionTest_getClassName() throws ClassNotFoundException {
// instance을 생성하지 않고 Metaspace 영역의 Type 정보를 가져온다.
for (int i = 0; i < 1000; i++) {
String name = Class.forName("notefive.oop.Animal").getSimpleName();
System.out.println(name);
}
}
// Non-Reflection API 17~22ms
@Test
public void reflectionTest_getName() {
// 생성된 instance를 기반 즉 Heap 영역의 Type 정보를 가져온다.
Animal animal = new Animal();
for (int i = 0; i < 1000; i++) {
String name = animal.getClass().getSimpleName();
System.out.println(name);
}
}
// method invoke
// Reflection API 34~46ms
@Test
public void reflectionTest_getClassAndGetMethod() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
for (int i = 0; i < 1000; i++) {
Object o = Class.forName("notefive.oop.Animal").newInstance();
Method getName = Class.forName("notefive.oop.Animal").getMethod("getName");
String res = (String) getName.invoke(o);
}
}
// Non-Reflection API 27~32ms
@Test
public void reflectionTest_getMethod() {
for (int i = 0; i < 1000; i++) {
String res = new Animal().getName();
}
}
}
Reflection API의 경우 초기 호출 시에 JVM의 Class Loader와 Excuter Engine을 통해 Class의 Metadata를 가져온 이후에는 Non-Reflection 방식과 동일하게 동작한다. 매번 인스턴스를 생성하고 메서드를 호출하는 절차가 진행하게 된다.
즉 초기 호출을 제외하고는 Reflection API를 사용하는 것이 별 차이가 없음을 알게 되었다.
특히 이미 Class Compile시 Loading 된 Class 정보를 Reflection API를 통해 가져오는 경우에는 기존 방식과 비교하여 오버헤드가 사실상 존재하지 않았다.
이러한 결과를 보았을 때 단순히 성능이 좋지 않다는 이야기 때문에 도입하지 못했던 것에 대해 Reflection API를 고려해볼 수 있는 어떠한 지표를 얻게 된 것 같다.
위에 작성된 getClassName 관련 테스트에선 Reflection API가 인스턴스를 생성하지 않고 정보를 가져오기 때문에, 인스턴스를 생성하여 정보를 가져오는 Non-Reflection API 방식이 일정 반복 횟수 이후에 GC가 발생하여 상대적으로 느린 실행시간을 보였었다.
HATEOAS를 만족시키진 않았습니다. 해당 내용과 관련해서 인프런에 백기선 님의 RESTful 강의를 수강해보시길 추천드립니다.
Entity, DTO 만들기
@Builder
@Getter
public class Notice {
private final Long id;
private final String author;
private final String title;
private final String content;
private final LocalDateTime createDate;
private final LocalDateTime modifyDate;
}
Builder 패턴과 Getter, final을 사용하여 불변 객체로 만듭니다. 이를 통해 변경될 수 있는 지점을 제거하고, 별도 동기화 없이 멀티스레드 환경에서 안전하게 사용하는 것이 주목적입니다.
불변 객체를 만드는 방법?
모든 필드를 Final로 만듭니다.
모든 필드를 비공개(Private)로 설정합니다.
필드에 대한 접근자(Getter)만을 제공합니다.
필드에 대한 변경자(Setter)를 제공하지 않습니다.
컬랙션이나 Date(변경되는 객체 등)를 사용하는 필드에 대하여서는 복사본이나 수정 불가능한 타입의 구현체로 반환합니다. Collections.UnmodifiableCollection 등
모든 코드가 멀티스레드에 취약한 것은 아닙니다. 상태를 가지는 싱글톤 객체를 사용할 때 , 다른 스레드가 가시 할 수 있는 필드 등에서 여러 변경을 시도하는 경우 문제가 발생할 수 있습니다.
해당 코드에서 사용된 Lombok Annotation
@Builder :객체 생성 방식을 빌더 패턴으로 제공합니다. 몇몇 다른 라이브러리 관점에서는 (Mybatis, Jackson 등) AllArgumentContructor와 동일한 취급을 받습니다.
이 DTO는 Client에서 넘어온 요청을 담는 객체입니다. 해당 프로젝트에서도 별도의 처리 없이 Data Transfer Object의 역할만을 담당하여 Controller와 Service를 이동하게 됩니다.
toEntity라는 Converting 메서드가 존재하는데요. 해당 DTO는 Service단에서 Entity로 변환되고 DAO로 접근하게 됩니다. 해당 메서드가 Entity에 있지 않고 DTO에 있는 이유는 Client와 밀접한 관계를 가지는 DTO의 요구사항 변경이 Entity에 영향을 주는 것을 방지하는 목적으로 작성하게 되었습니다.
해당 코드에서 사용된 Lombok Annotation
@AllArgsConstructor :객체의 모든 필드를 가지는 생성자를 만드는 어노테이션입니다.
@NoArgsConstructor :인자가 없는 생성자를 만드는 어노테이션입니다.
해당 코드에서 사용된 javax의 validation Annotation
@NotBlank :String에 적용되는 Annotation으로 null, "", " "인지 확인합니다. 조건이 충족된다면 MethodArgumentNotValidException이 발생하게 됩니다.
문자열과 관련해서 NotNull과 NotEmpty가 합쳐진 Annotation이라고 보아도 무방합니다.
MethodArgumentNotValidException?
해당 Exception은 Validation Annotation에 의해 발생하게 되는 RuntimeException으로 특이하게 BindingResult 타입의 필드 변수가 존재하는데요. 해당 변수에는 Validation을 통과하지 못한 필드 명과 메시지가 저장되게 됩니다.
UpdateNoticeDto
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class UpdateNoticeDto {
@NotBlank
private String author;
@NotBlank
private String title;
@NotBlank
private String content;
}
CRUD 중 C! 객체를 DB에 저장하는 로직입니다. 단순히 저장만 할 것이라면 쿼리와 인자만을 사용해도 되지만, 위와 같이 Key(Notice ID) 값을 클라이언트에게 넘겨줌으로써 그에 대한 정보나, 상태를 가질 수 있도록 할 수 있습니다.
예를 들어 HATEOAS를 지원하는 RESTful API라면, 현재 호출된 URI가 api/notices 일 텐데요. LinkBuilder를 이용하여서 클라이언트에게 api/notices/{생성된 id}를 전달할 수 있고, 클라이언트는 이 링크를 사용만 함으로써 쉽게 생성된 정보를 확인할 수 있는 페이지로 전환 가능할 것입니다.
Notice를 조회하기 + RowMapper
public List<Notice> findAll(Long page, Long offset) {
final String sql = "SELECT * FROM NOTICE ORDER BY CREATE_DATE DESC LIMIT " + page + " OFFSET " + offset;
return jdbcTemplate.query(sql, rowMapper());
}
private RowMapper<Notice> rowMapper() {
return (rs, rowNum) -> Notice.builder()
.id(rs.getLong("ID"))
.author(rs.getString("AUTHOR"))
.title(rs.getString("TITLE"))
.content(rs.getString("CONTENT"))
.createDate(rs.getTimestamp("CREATE_DATE").toLocalDateTime())
.modifyDate(rs.getTimestamp("MODIFY_DATE").toLocalDateTime())
.build();
}
CRUD 중 R! DB에서 조건에 맞는 객체를 가져와 클라이언트에게 전달하는 로직입니다. 쿼리에 LIMIT와 OFFSET을 이용함으로써 간단한 페이징 기능을 구현하였습니다.
RowMapper는 반환되는 ResultSet을 객체로 변환하는 로직을 구현하는 것입니다. 각각의 결과 행들을 Mapping 합니다.
Notice를 수정하기
public int updateById(UpdateNoticeDto noticeDto, Long noticeId) {
final String sql = "UPDATE NOTICE SET AUTHOR = ?, TITLE = ?, CONTENT = ?, MODIFY_DATE = ? WHERE ID = ?";
return jdbcTemplate.update(sql, noticeDto.getAuthor(), noticeDto.getTitle(),
noticeDto.getContent(), LocalDateTime.now(), noticeId);
}
CRUD 중 U! Client에서 전달한 notice의 수정 정보를 DB에 반영하는 로직이 작성되어 있습니다.
Notice를 삭제하기
public int deleteById(Long id) {
final String sql = "DELETE FROM NOTICE WHERE ID = ?";
return jdbcTemplate.update(sql, id);
}
마지막으로 CRUD 중 D! Client에서 전달한 Notice ID를 삭제하는 로직이 작성되어 있습니다. 현재 작성된 로직은 Hard Delete 방식으로 바로 데이터를 삭제하게 되는데, 다른 방법으로는 Soft Delete라는 것이 존재합니다.
Soft Delete는 테이블 칼럼의 Flag를 변경하여 최종 결과에서 필터링하거나, 별도의 테이블에 데이터를 이동시켜 관리하는 것으로, Batch와 조건식을 통해서 일정 시간, 일정 상황에 데이터가 삭제되게끔 작성할 수 있습니다.
Domain Service 만들기
해당 프로젝트에서 Service는 단순하게 Controller와 DAO를 분리하는 Layer의 용도로만 작성되었습니다. 해당 내용에 대해서는 계층형 아키텍처를 참고하는 것이 좋습니다.
Service Layer의 주요 목적은 비즈니스 로직 수행, 트랜잭션 관리(글로벌 트랜잭션 경계 설정 등), 접근 권한 확인, Controller와 DAO의 결합 분리 등이 있다고 생각합니다.
Service에서 save 호출하기
@Service
public class NoticeService {
private final NoticeDao noticeDao;
public NoticeService(NoticeDao noticeDao) {
this.noticeDao = noticeDao;
}
public int save(CreateNoticeDto noticeDto) {
Notice notice = noticeDto.toEntity();
int result = noticeDao.save(notice);
if (isNotReflected(result)) {
throw new RuntimeException("Notice save Failed");
}
return result;
}
크게 복잡한 로직은 존재하지 않습니다. DTO를 Entity로 Converting 하고 DAO의 save를 호출합니다. 호출된 결과를 int 값으로 받게 되는데, 1 이상이 아니라면 결과가 반영되지 않았으므로 Runtime Exception을 발생시키게 됩니다.
Service 로직에서 발생하는 Exception들은 이후 ExceptionHandler를 통해 Handling 합니다.
@Service Annotation은 Component를 확장한 MetaAnnotation 중 하나로 ComponentScan의 대상입니다.
Service에서 findAll 호출하기
public List<Notice> findAll(Long page, Long offset) {
return noticeDao.findAll(page, offset);
}
Dao의 findAll을 호출하고 그 결과를 Controller로 전달합니다.
Service에서 update 호출하기
public void updateById(UpdateNoticeDto noticeDto, Long noticeId) {
if (isNotReflected(noticeDao.updateById(noticeDto, noticeId))) {
throw new RuntimeException("Notice update Failed");
}
}
save와 마찬가지로 로직을 호출하고 그 반영 결과를 검증하여 Runtime Exception을 발생시킵니다.
Service에서 delete 호출하기
public void deleteById(Long noticeId) {
if (isNotReflected(noticeDao.deleteById(noticeId))) {
throw new RuntimeException("Notice delete Failed");
}
}
update와 동일함으로 넘어가겠습니다.
Service의 결과 검증 메서드
private boolean isNotReflected(int result){
return result < 1;
}
Domain Controller 만들기
Controller의 save
@RestController
@RequestMapping("/api")
public class NoticeController {
private final NoticeService noticeService;
public NoticeController(NoticeService noticeService) {
this.noticeService = noticeService;
}
/**
* 사용자의 글 작성
*
* @return 작성된 notice 에 대한 id 반환
* @author lob
*/
@PostMapping("/notices")
public ResponseEntity<NoticeInfo> createNotice(@Valid @RequestBody CreateNoticeDto noticeDto) {
int result = noticeService.save(noticeDto);
return ResponseEntity.status(HttpStatus.OK).body(new NoticeInfo(result, "notice created"));
}
Controller 코드의 최상단입니다. Class Level에는 @RequestMapping("path 정보")를 적용하여 모든 메서드의 URL prefix를 설정하였습니다. 이후 NoticeService를 생성자 주입으로 DI 받고, 메서드를 호출합니다.
createNotice는 @PostMapping("/notices")로 설정되어 있는 상태인데, 이는 POST.../api/notices 형태의 요청과 Mapping 되는 것임을 나타냅니다.
메서드의 인자로 DTO를 받고 해당 DTO에 대한 @Valid와 @RequestBody를 적용하였습니다. 이는 json으로 된 요청 정보를 DTO 생성 후 매핑하고, DTO Field Level에 설정된 Valid Annotation을 통해 Validation을 진행한다는 것을 나타냅니다. @NotBlank, NotEmpty..
json Mapping 정보를 DTO로 Mapping 할 때 필드가 private로 캡슐화되어 있는 상태라면, Getter를 통해 필드 이름을 특정하고 직렬 화합니다.
해당 코드에서 사용된 Spring Annotation
@RestController :Controller와 ResponseBody 기능이 Annotation입니다.
@Controller는 요청을 받고 결과를 반환하는 역할을 하는 Bean을 등록할 때 사용되며 내부적으로 Component Annotation이 적용되어 있습니다.
@ResponseBody는 컨트롤러가 반환하는 결과를 Http Message Body에 저장합니다.
JSON 형식을 반환한다라는 글들이 많은데, 실제로는 HTTP Header의 Content-type 값을 따릅니다. 즉 byte 값, XML, TEXT 등으로도 반환된다는 것입니다.
@PostMapping :HTTP POST Method 형식을 처리하는 것을 나타내는 Annotation입니다. 이는 RequestMapping을 확장한 것이며, URL 값을 나타내는 value, Headers, 요청 타입을 협상하고 반환 타입을 지정하는 produces 필드를 자주 사용하게 됩니다.
@RequestBody :요청된 HTTP Message Body에 저장된 값을 직렬 화하여 객체로 변환하는 데 사용되는 Annotation입니다. JSON, XML, Text 등을 가져올 수 있습니다.
@Valid :Object 필드에 선언된 Valid 조건들을 검증하도록 하는 Annotation입니다.
Controller의 findAll
/**
* @return 작성된 순서대로 10개씩 notice 반환
* @author lob
*/
@GetMapping("/notices")
public ResponseEntity<List<Notice>> findAll(
@RequestParam(defaultValue = "10", required = false) Long page,
@RequestParam(defaultValue = "0", required = false) Long offset) {
List<Notice> notices = noticeService.findAll(page, offset);
if (CollectionUtils.isEmpty(notices)) {
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
return ResponseEntity.status(HttpStatus.OK).body(notices);
}
findAll은 @GetMapping("/notices")으로 설정된 상태인데 이는 GET.../api/notices 형태의 요청과 Mapping 됨을 알 수 있습니다.
해당 코드에서 사용된 Spring Annotation
@GetMapping *: HTTP GET Method 형식을 처리하는 것을 나타내는 Annotation입니다. 이것도 RequestMapping을 확장한 Annotation입니다.*
@RequestParam :HTTP URL에 붙어서 날아오는 QueryString을 변수에 매핑하는 Annotation입니다. defaultValute를 통해 요청에 담겨오지 않는 경우의 값을 설정할 수 있으며, required를 통해 요청에 QueryString 존재 유무에 따라서 Exception을 발생시킬지를 설정할 수 있습니다.
required의 기본 값은 True입니다.
Controller의 update
/**
* @return notice 수정 후 안내 문자열 반환
* @author lob
*/
@PutMapping("/notices/{noticeId}")
public ResponseEntity<String> updateById(@Valid @RequestBody UpdateNoticeDto noticeDto,
@PathVariable Long noticeId) {
noticeService.updateById(noticeDto, noticeId);
return ResponseEntity.status(HttpStatus.CREATED).body("Notice Updated");
}
updateById은 @PutMapping("/notices/{noticeId}")으로 설정된 상태인데 이는 PUT.../api/notices 형태의 요청과 Mapping 됨을 알 수 있습니다.
해당 코드에서 사용된 Spring Annotation
@PutMapping :HTTP PUT Method 형식을 처리하는 것을 나타내는 Annotation입니다. RequestMapping을 확장하였습니다.
Controller의 delete, NoticeInfo Object
/**
* @return notice 삭제 후 안내 문자열 반환
* @author lob
*/
@DeleteMapping("/notices/{noticeId}")
public ResponseEntity<String> deleteById(@PathVariable Long noticeId) {
noticeService.deleteById(noticeId);
return ResponseEntity.status(HttpStatus.OK).body("Notice Deleted");
}
@Getter
@AllArgsConstructor
private static class NoticeInfo {
private final int NoticeId;
private final String message;
}
deleteById은 @DeleteMapping("/notiecs/{noticeId}")으로 설정된 상태인데 이는 Delete.../api/notieces 형태의 요청과 Mapping 됨을 알 수 있습니다.
해당 코드에서 사용된 Spring Annotation
@DeleteMapping :HTTP DELETE Method 형식을 처리하는 것을 나타내는 Annotation입니다. RequestMapping을 확장하였습니다.
Exception Handler 적용해보기
@ControllerAdvice("com.example.rest.notice")
public class NoticeExceptionHandler {
@ExceptionHandler(RuntimeException.class)
protected ResponseEntity<ErrorResponse> HandlerRuntimeException(RuntimeException exception) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(exception.getMessage()));
}
@Getter
@AllArgsConstructor
private static class ErrorResponse {
private final String errorMessage;
}
}
간단하게 작성한 Handler입니다. Service에서 발생하는 RuntimeException에 의한 White Page를 방지하고, Client에게 별도의 데이터를 제공합니다.
예제의 간소화를 위하여 RuntimeException 형식만을 지정하였습니다.
해당 코드에서 사용된 Spring Annotation
@ControllerAdvice :Spring Application에서 전역적인 예외 처리를 위해 사용되는 객체에 적용하는 Annotation입니다. Controller에서 결과를 반환한 이후 즉 AfterReturning 시점에서 적용되며, 내부에 정의된 ExceptionHandler 설정에 따라 처리하게 됩니다.
Class Level Annotation이며, 특정 Package에만 적용하는 것이 가능하고 Order Annotation을 통해 적용 우선순위도 지정할 수 있습니다.
@ExceptionHandler :Spring Application에서 특정 예외 처리를 위해 사용되는 Method Level의 Annotation입니다. 기본적으로 @ExceptionHandler(XxxException.class) 형식으로 정의되어 해당 Exception을 가로채고 매개변수로 받아올 수 있습니다.
기존에 사용되던 final static keyword를 사용한 변수 선언 등을 대체하였다.
해당 키워드의 특징으로는
모든 Java의 enum들은 암시적으로 java.lang.Enum Class를 확장한다.
정의된 상수 하나당 하나의 인스턴스가 생성된다. public static final type instance
데이터를 비교할 때 실제 값과 함께 타입을 체크한다.
메서드와 필드를 추가하고 인터페이스를 구현할 수 있다.
다른 Primitive Type Variable 과 같이 Switch Statement에서 사용할 수 있다.
정의 시 Class Level에서 사용된다.
정도가 있다.
enum 선언, 할당 방식
JDK 5.0 이전의 방식
Class Role {
public final static int ADMIN = 0;
public final static int USER = 1;
public final static int GUEST = 2;
}
일반적인 선언, 할당
// 선언
public enum Role {
ADMIN, USER, GUEST
}
// 할당
Role role;
role = Role.ADMIN;
role = Role.USER;
role = Role.GUEST;
enum Class 내부에 Method, Variable 선언
public enum Role {
ADMIN, USER, GUEST;
//내부 변수
private String a;
private String b;
//생성자
Role(String a, String b) {
this.a = a;
this.b = b;
}
public String getA() {
return a;
}
public String getB() {
return b;
}
}
enum 사용 방식
조건문 : if
if (role == Role.USER) {
System.out.println("USER");
}
if (role == Role.GUEST) {
System.out.println("GUEST");
}
if (role == Role.ADMIN) {
System.out.println("ADMIN");
}
조건문 : Switch
switch (role) {
case USER :
System.out.println("USER");
break;
case GUEST :
System.out.println("GUEST");
break;
case ADMIN :
System.out.println("ADMIN");
break;
}
반복문 : for-each
for (Role role : Role.values()) {
System.out.println(role);
}
enum이 제공하는 메서드 (values()와 valueOf())
컴파일러가 자동으로 추가해주는 메서드들이라고 한다.
values()
정의된 모든 상수를 배열에 담아 반환한다.
Role[] role = Role.values();
for (Role r : role) {
System.out.println(r);
}
for (Role role : Role.values()) {
System.out.println(role);
}
valueOf()
정의된 상수와 변수로 넘긴 문자열을 비교한 뒤 그 결과를 반환한다.
전달된 문자열과 동일한 상수가 없을 경우 NPE가 발생한다.
role = Role.valueOf("USER");
// NPE
role = Role.valueOf("aaa");
기타 메서드
ordinal()
현재 상수 값이 선언된 enum Class 내에서 어느 위치(index)에 있는지 반환하는 메서드이다.
상수의 위치가 변경되거나 새로운 상수가 추가된다면 해당 메서드를 사용한 로직들은 깨지게 된다.
int idx = role.ordinal();
compareTo()
상수의 ordinal 값을 이용하여 두 개의 상수를 비교하는 메서드이다. 반환 값은 int이다.
Web client와 Server 간의 데이터 전송을 위해 사용되는 Application Layer Protocol입니다.
요청과 응답을 하나의 트랜잭션 단위로 묶어놓고, 응답 이후에는 별도의 정보나 상태를 가지지 않는 Stateless Protocol이고, 데이터를 평문으로 전송되게 구현되어 있습니다.
사용 시 고려해야 할 점
Stateless 하기에 상대적으로 서버의 Resource를 적게 사용할 수도 있으나, TCP Connection에 대한 Overhead가 심화될 수 있기에 이를 최적화하기 위한 여러 방법을 고려해야 합니다.
데이터가 평문으로 전송되기에 패킷 탈취, 변조에 대한 보안 공격에 취약점을 가지기에, HTTP Over SSL(HTTPS)과 같은 방법을 사용하여야 합니다.
추가적으로 학습할 Keyword로는 Handshaking, HTTP 구조와 1.1, 2.0, quic 정도가 있습니다.
HTTP Method
GET (멱등성 O)
Resource를 조회하는 상황에서 사용하는 HTTP Method입니다. 이 요청에 대해서는 캐싱이 가능하고 요청 Body가 기본적으로 제공되지 않습니다. 이는 요청 Body를 이용할 수 있음을 의미합니다.
몇 번을 요청하더라도 호출의 결과는 같기에 멱등성을 지킨다고 이야기합니다. 이 요청을 통해 받을 수 있는 State code는 200, 400, 404 등이 있습니다.
POST (멱등성 X)
Resource를 생성하는 상황에서 사용하는 HTTP Method입니다. 이 요청에 대해서는 캐싱이 가능하고 요청 Body가 존재합니다. 해당 요청 시 Resource를 만들 수 있는 충분한 정보를 가져야 합니다.
요청 시마다 새로운 Resource가 생성되기에 멱등성을 지키지 못한다고 이야기합니다. 받을 수 있는 State code는 201, 204 등이 있습니다.
DELETE (멱등성 O, X)이 요청은 구현 방식에 따라 멱등성을 지킬 수도 있고, 지키지 못할 수도 있습니다. 기본적인 제약으로는 멱등성을 지킨다고 하는데, 이를 만족시키기 위해서는 해당 요청에 따라 Resource가 바로 삭제되는 것이 아닌 Flag 등을 통한 Soft Delete가 되어야 합니다.
Resource를 삭제하는 상황에서 사용하는 HTTP Method입니다. 이 요청에 대해서는 캐싱이 불가능하고 요청 Body가 존재하지 않습니다.
If the target resource has one or more current representations, they might or might not be destroyed by the origin server, and the associated storage might or might not be reclaimed, depending entirely on the nature of the resource and its implementation by the origin server (which are beyond the scope of this specification).
PUT (멱등성 O)
Resource를 전체적으로 수정할 때 사용하는 HTTP Method입니다. 이 요청에 대해서는 캐싱이 불가능하고 요청 Body가 존재합니다.
이 요청은 Resource가 존재한다면 모든 정보를 수정하고, 존재하지 않는다면 새로운 Resource를 생성하게 됩니다. 즉 POST와 같이 Resource 생성 시 필요한 모든 정보를 포함하고 있어야 합니다.
PATCH (멱등성 X)
Resource의 일부 정보를 수정하기 위해 사용하는 HTTP Method입니다. 이 요청에 대해서는 캐싱이 가능하고 요청 Body가 존재합니다.
이 요청은 Resource가 존재한다면 일부 정보를 수정하고, 존재하지 않는다면 Update 되지 않을 수 있습니다. 매 실행마다 다른 결과를 받을 가능성이 존재하기에 기본적으로 멱등하지 않다고 이야기합니다.
이와 같이 Key : Value Format을 사용합니다. 추가적으로 UTF-8 Encoding을 제공하며, 별도의 주석을 제공하지 않습니다.
Client-Server Model
Resource 요청자와 Resource 제공자 간에 작업을 분리하는 분산 애플리케이션이자 네트워크 아키텍처를 의미합니다. 일반적으로 웹 페이지 ↔ 서버 관계를 말합니다.
이외에도 알아볼 Keyword로는 DNS, Router, TCP/IP, UDP, Packet 등이 있습니다.
REST이란? (REpresentational State Transfer)
REpresentational State Transfer : 리소스의 상태를 표현하고 전송한다.
REST란 WWW과 같은 분산 하이퍼미디어 시스템에서 리소스와 상태를 표현하고, 전송하는 것에 대한 설계 양식입니다.
많은 사람들은 HTTP Protocol을 통해 REST를 구현하고 있지만, 이것은 HTTP에 종속되지 않는 개념입니다. 즉 다른 Protocol을 사용하거나 새로운 Protocol을 만들더라도 해당 설계 원칙을 지키면 "RESTful 하다."라고 말할 수 있음을 의미합니다.
해당 내용에 대해서는 CoAP RESTful API를 검색해보시면 좋을 것 같습니다.
REST의 6대 제약
Client-Server Model
Client - Server 아키텍처와 같이 독립적인 상태에서 구현되어야 하는 API 규약입니다.
Stateless
매 요청은 필요한 모든 정보를 담고 있어야 하며, 종료 시 상태는 없어야 한다.
Cache
SELECT - GET과 같은 조회성 트랜잭션은 캐싱하여야 한다.
Uniform Interface
제공되는 인터페이스는 일반적이고, 일관성이 있어야 하며, 간단할수록 좋습니다.
Identification of resourcesREST API Design 항목 참고
각 Resource은 유일하게 식별 가능해야 하며, 개념적으로 분리되어야 합니다.
Manipulation of resources through representationsClient는 매 요청에 대한 충분한 정보를 제공하여야 합니다. 수정, 조회, 삭제를 위한 데이터
HTTP Method로 CRUD라는 표현을 담아야 합니다. URI에 담지 않습니다.
Example
GET "http://www.example.com/api/users" HTTP/1.1
PATCH "http://www.example.com/api/users/1224" HTTP/1.1
이와 같이 URI 정보에는 CRUD 표현을 담지 않고, HTTP Message의 Method를 통해 표현해야 합니다.
Self-describing messages
메시지 스스로 자신에 대한 설명이 가능해야 합니다.
Resource를 제공할 때 Resource의 Type을 제공하고 (Content-type), Resource에 대한 링크를 Response Body에 포함함으로써 해당 조건을 만족할 수 있습니다.
Hypermedia as the engine of application state (HATEOAS)
(post들 중에 하나의 post)
"https://www.example.com/api/posts/{postId}" -> "api/posts/123"
(location들 중에 하나의 location)
"https://www.example.com/api/loacations/{locationId}" -> "api/loacations/123"
(user들 중에 하나의 user)
"https://www.example.com/api/users/{userId}" -> "api/users/123"
하위 리소스 표현
해당 Resource가 보유하고 있는 하위 Resource에 대해 작성하는 방식입니다.
( Customer들 중에 하나의 Customer 정보 )의( accounts 정보 들)
"https://www.example.com/api/customers/{customerId}/accounts"
( Customer들 중에 하나의 Customer 정보 )의( accounts 정보 들 중 하나의 accounts )
"https://www.example.com/api/customers/{customerId}/accounts/{accountsId}"
버저닝
변경 사항에 따른 API 버전 관리 방식입니다.
Semantic Versioning
{MAJOR}. {MINOR}. {PATCH} 형식으로 Version을 관리하는 방식을 의미합니다.
Index는 책의 목차, 색인과 같은 역할을 담당하는 데이터베이스 객체로써 테이블과 독립적으로 존재합니다. 하지만 테이블에 의존적이기에 해당 테이블이 삭제될 경우 같이 제거되게 됩니다.
존재하는 칼럼의 값과 해당 레코드가 저장된 주소를 키와 값의 구조로 묶어 저장하고, 정렬된 상태를 유지하기에 저장, 수정, 삭제 기능들의 성능을 희생하고 빠른 조회를 제공하는 것이 Index의 사용 목적입니다.
기본 제공되는 Index?
기본적으로 제공되는 Index는 PK index 입니다. InnoDB는 설계상 이유 때문에 모든 테이블에 PK가 필요한데요. 개발자가 테이블에 PK를 작성하지 않는다면, 암시적으로 PK를 생성하여 레코드를 탐색하고 인덱스를 생성하는데 사용되게 됩니다.
Primary Key vs Secondary Key
Primary Key : 테이블마다 기본 키 제약 조건을 통해 만들어지는 하나의 고유한 Key를 의미하고, Null 값과 중복 값을 허용하지 않으며, 앞서 이야기한 것처럼 자동으로 인덱스를 생성하여 데이터에 대한 빠른 접근을 지원합니다.
Secondary Key : 각 레코드에 대한 고유한 값을 제공하는 Key를 의미하는데, Null을 허용하며, 레코드를 식별하는데 사용할 수 있고 Index로 활용할 수 있습니다. 하나의 테이블에 여러 Key가 존재할 수 있습니다.
Unique Index를 사용하는 이유?
Index에도 Unique Index와 Non-Unique Index가 존재하는데요. 간단하게 Unique Index를 알아봄으로써 어떠한 차이를 가지는지 확인해보겠습니다.
Unique Index : 하나의 Key만 존재함을 나타냅니다. 이는 Index에 대해 동등 조건을 사용하는 쿼리에서 DB의 쿼리를 실행하고 최적화하는 옵티마이저에게 하나의 Index를 찾았을 때 더 이상 스캔하지 않아도 된다는 의미를 제공하게 되며, 이를 통해 쿼리 최적화를 수행하게 되는데요. 그렇기에 사용 가능한 모든 경우에서는 Unique Index를 사용하는 것이 권장되게 됩니다.
추가적으로 고유한 인덱스를 사용하는 것은 해당 레코드의 데이터 무결성을 보장하게 됩니다.
index의 장점?
조회(SELECT) 쿼리의 성능을 향상시킵니다. 여러 서비스에선 데이터를 조회하여 사용자에게 제공하는 것이 다른 쿼리보다 많은 비중을 차지하기 때문에 도입하는 경우도 있습니다..
고유한 index 형식을 사용한다면 이는 행에 대해서도 중복 없이 구성하는 것을 보장합니다.
매번 테이블을 스캔한 후 행을 정렬하는 절차를 생략하게 합니다. 미리 정렬된 목록을 제공함으로써 매 쿼리마다 정렬을 하지 않아도 빠르게 데이터를 검색할 수 있도록 지원하게 됩니다.
DB는 기본적으로 모든 행을 스캔하고 정렬한 뒤 일치하는 행을 필터링하여 결과를 반환합니다.
index의 단점?
데이터의 수정(insert, update, delete)이 발생할 때마다 연관 index도 업데이트해야 합니다. 즉 다른 쿼리의 성능이 떨어지게 되는 문제점이 있습니다. update의 경우 where 조건에 index를 사용하고 있다면 해당 칼럼을 찾아 변경하는 성능을 높일 수 있습니다. (index update는 동일하다.)
Index는 DB 내에서 별도의 저장 공간을 차지합니다.
Index를 유지하는 비용이 발생합니다. 대표적으로 데이터 수정 , 삭제, 추가 등의 경우 Index가 업데이트되다가 깨지는 경우가 발생하게 되는데 이때 복구, 정상적인 Index로의 우회 등 추가적인 처리가 필요합니다.
Index Corruption?
Corrupt Index는 DDL, DML(insert, update, delete) 쿼리를 수행한 뒤 정렬하는 도중이나, Slow Query에 의한 비정상적인 종료에 의해 발생할 수 있습니다. 하지만 확실하게 손상되었는지 확인할 수 없기 때문에, 이를 파악하기 위해서는 테스트를 통해 실행시간과 결과를 관찰하여야 합니다.
index를 사용하여야 하는 시점?
테이블에 인덱스를 추가하는 것은 저장 속도를 어디까지 희생하고, 읽기 속도를 얼마나 더 빠르게 만들어야 하는지의 여부에 따라 결정되어야 합니다.
인덱스를 추가하거나 제거할 때마다 성능 테스트를 수행하여 어떤 영향이 미치는지 실질적인 수치를 파악하여야 하고,
이는 많은 검색 쿼리에서 빈번하게 키로 사용되는 컬럼을 인덱스로 사용한다는 것이 기본 전제가 되는데요, 이를 정량화(수치로 만들어서)하여 판단해야 한는 것이 좋습니다.
index의 알고리즘?
간단하게 Index 알고리즘의 종류와 개념만 작성하였습니다.
B-Tree Index : balanced Tree를 사용하는 방식을 의미하며, 일반적으로 사용되는 유형입니다. 칼럼의 값을 변형하지 않고, 원래의 값만을 이용해 Indexing 하는 특성을 가지게 됩니다.
여러 형태의 변형된 알고리즘을 가집니다. B+-Tree, B-Tree 등*
Hash Index : 컬럼의 값을 Hash 값으로 계산하여 Indexing 하는 방식입니다. 매우 빠른 검색을 지원하나 일부 값을 통해 값을 찾는 Pattern matching을 지원하지 못합니다. 주로 메모리 기반의 DB에서 사용됩니다.
Fractal-Tree Index : B-Tree 방식의 단점을 보완하기 위해 고안된 방식입니다. 값을 변경하지 않고 인덱싱 하는 것은 동일하나 데이터가 저장되거나 삭제될 때 발생하는 비용 (Disk I/O)을 줄이도록 설계된 것이 특징입니다. 각 내부 노드에 버퍼를 포함함으로써 데이터를 임시로 저장하고 버퍼가 가득 채워졌을 때 Flush 하게 됨으로 I/O 작업 단위를 크게 만들어서 유지하게 됩니다.
R-Tree Index : 2차원 데이터를 Indexing 하고 검색하는 목적을 지니는 방식입니다. 주로 공간 개념 값을 사용하는 GPS나 GIS 서비스에서 사용하게 됩니다.
Full Text Search Index : 문서의 내용 전체를 Indexing 하여 특정 키워드가 포함된 문서를 분석, 검색하는 방식에서는 B-Tree 형식을 사용할 수 없기에 사용되는 방식입니다. 크게 Stopword 방식과 N-Gram 방식으로 이야기할 수 있습니다.
중괄호 위치 등의 Coding Convention을 제한하고 메서드 명명 규칙을 기능으로써(?) 제공하는 부분이 정말 재미있었는데요. 이미 기존에 학습하시고 사용하시는 분들이라면 김 빠지실 수 있겠지만, 이런 부분이었습니다.
// private method, 외부 접근 불가능
func sayBye() {
fmt.Println("bye")
}
// public method, 외부 접근 가능
func SayHello() {
fmt.Println("hello")
}
소소한 즐거움..?
현재 자바를 이용해 프로젝트를 하며 드는 생각은 문법이 읽기 좋지만, 너무 많은 코드를 작성하게 된다라는 점이 있는데요. Go는 간단한 문법을 내세우는 언어답게 간결하면서도 제가 좋아하는 정적, 강타입도 지키는 언어이기에 정말 마음에 들었습니다. (변수 타입은 컴파일 시점에서 결정되지만요.)
// Java : 1
public static void main(String[] args) {
int a = 0, i;
for (i = 0; i < 10; i++) {
a += i;
}
System.out.println(a);
}
// Java : 2
public static void main(String[] args) {
int a = 0, i = 0;
while (i != 10) {
a += i;
i++;
}
System.out.println(a);
}
// Go
func main() {
sum, i := 0, 0
for i < 10; {
sum += i
i++
}
fmt.Println(sum)
}
이런 예제에서는 간결함을 크게 느낄 수는 없겠지만요.
물론 없는 문법도 많고.. (예외 처리라던지?) 핵심 라이브러리도 부족하다고 하지만, 모든 언어가 모든 상황에서 좋은 경우는 없기 때문에 (그렇게 믿고 있습니다.) 충분히 감내할 수 있는 부분이라고 생각합니다.
여유가 생길 때마다 새로운 기술과 언어를 꾸준하게 공부할 생각인데 이러한 마음가짐이 계속 갔으면 좋겠습니다. ㅎㅎ
이번 한 주 다들 노고가 많으셨고요. 주말에 푹 쉬시고 다음 주도 힘차게 나아가 봅시다! 파이팅!