DB를 어떻게 설계하고, 객체를 어떻게 설계해서 중간에서 JPA를 어떻게 사용해서 mapping 할 것 인지
영속성 컨텍스트
실제 JPA가 내부에서 어떻게 동작하는지
영속성 컨텍스트를 이해하면 JPA가 내부적으로 어떻게 돌아가는지 이해할 수 있을 것
Entity Manager Factory와 Entity Manager
예를들어 웹 어플리케이션을 개발할 때 사용자의 요청에 따라서 EMF를 통해 EM을 생성합니다. EM은 내부적으로 DB 커넥션을 통해 DB를 사용합니다.
그렇다면 영속성 컨텍스트는 무엇일까??
영속성 컨텍스트
JPA를 이해하는데 가장 중요한 용어
"엔티티를 영구 저장하는 환경"이라는 뜻
EntityManager.persist(entity);
해당 코드의 의미는 DB에 저장되지만 DB에 저장한다는 의미가 아닌 영속성 컨텍스트에 저장한다는 의미입니다. (아직 어떤 느낌인지 모르겠습니다... ?!?!?!?) => 실제 확인해보니 em.persist할 때 쿼리문이 나가지 않고 쓰기 지연 SQL 저장소에 쿼리가 저장되고 commit이나 flush를 할 경우 DB에 쿼리문을 보내어 DB를 동기화 시켜줌
EM? 영속성 컨텍스트?
영속성 컨텍스트는 논리적인 개념
눈에 보이지 않기때문에 이해하기 쉽지 않다.
EM을 통해 영속성 컨텍스트에 접근
EM과 영속성 컨텍스트는 J2SE 환경에서 1:1로 매핑됩니다.
스프링 프레임워크같은 환경에선 N:1로 매핑되는데 해당 부분은 다음에 이해하는 것을 권장
Entity의 생명주기
비영속 (new/transient)
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
영속 (managed)
영속성 컨텍스트에 관리되는 상태
준영속 (detached)
영속성 컨텍스트에 저장되었다가 분리된 상태
삭제 (removed)
삭제된 상태
비영속
객체만 생성하고 em에 persist하지 않은 상태
영속
객체를 EM에 저장한 상태 (트랜잭션에 commit하지 않았기때문에 DB에는 올라가지 않는다.)
준영속, 삭제
영속성 컨텍스트의 이점
1차 캐시
동일성(Identity) 보장
트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
변경 감지(Dirty Checking)
지연 로딩(Lazy Loading)
애플리케이션과 DB 사이에 중간 계층이 있음으로서 위와같은 이점을 얻을 수 있다. 영속성 컨텍스트 뿐만 아니라 중간에 하나를 더 두면서 얻는 이점은 버퍼링, 캐시 등과 같은 이점을 얻을 수 있다.
엔티티 조회, 1차 캐시
이렇게 @Id로 member1이 있고, Entity로 객체가 있음으로서 얻는 이점은 1차 캐시에서 조회할 때 있습니다.
1차 캐시에서 조회
em.persist로 1차 캐시에 저장이 되었다고 가정한뒤, 조회한다고 합시다. 조회를 할 경우 DB를 먼저 조회하는 것이 아닌 1차 캐시에서 먼저 조회를 하게 됩니다. 그때 1차 캐시에서 원하는 객체가 있는 경우 DB를 조회하지 않고 바로 객체를 반환 받을 수 있습니다.
데이터베이스에서 조회
조회할 때, 1차 캐시에 없는 경우 DB를 조회하게 됩니다. 이때, DB에 원하는 객체가 있는 경우 1차 캐시에 저장한 뒤, 1차 캐시에 있는 객체를 반환하게 됩니다.
EM을 사용함으로서 위와같은 1차캐시의 이점을 얻을 수 있지만, 현업자분의 말로는 큰 이점은 없다고 합니다. 이유는 EM은 DB 트랜잭션 단위로 만들고 트랜잭션 종료시 종료됩니다. 이 말은 고객의 요청에 따라 비즈니스가 끝나게 될 경우 영속성 컨텍스트를 지운다는 뜻 입니다. 이렇듯 굉장히 찰나에 순간에만 이점을 얻을 수 있기 때문에 사실상 큰 이점은 없다고 합니다.
영속 엔티티의 동일성 보장
영속성 컨텍스트에서 같은 객체를 불러와 비교해보면 자바 컬렉션에서 똑같은 레퍼런스 두개를 가져와 비교할 때 같다고 나오는 것 처럼 같다고 나옵니다.
엔티티 등록
트랜잭션을 지원하는 쓰기 지연
트랜잭션 commit을 하기전까진 1차 캐시에 변경된 내용은 쓰기 지연 SQL 저장소에 쿼리문이 차곡차곡 쌓이게 됩니다. Queue와 비슷한 형식인 것 같습니다. 후에 트랜잭션 commit을 한 경우 쌓인 쿼리문은 flush가 되며 DB에 저장하게 됩니다.
엔티티 수정
변경 감지
데이터를 수정할 때 객체를 가져와 수정 후 update나 persist를 다시 해줘야하는 것 아닌가라는 의문이 생길 수 있습니다. (저도 그랬습니다) 하지만, JPA를 사용하는 목적은 자바에서 컬렉션을 사용하는 것처럼 사용하기 위함 입니다. 컬렉션을 수정할 때, 수정 후 다시 데이터를 집어넣는 작업을 해야하는가를 생각해보면 그렇지 않습니다. 그렇기때문에 JPA에서도 데이터를 수정 후 집어넣는 작업을 하지 않아도 됩니다. 이것이 가능한 이유는 JPA에서 Dirty Checking을 하기 때문입니다.
변경 감지 (Dirty Checking)
JPA는 DB 트랜잭션 커밋하는 시점에 내부적으로 flush()가 호출됩니다. 후 엔티티와 스냅샷을 비교합니다. 1차 캐시 안에는 사실 PK값, 객체 뿐만아니라 스냅샷도 있습니다. 스냅샷은 값을 읽는 최초 시점을 저장한 객체입니다. 그렇기 때문에 flush()가 호출 되어 엔티티와 스냅샷을 비교할 때 최초 시점에 객체와 commit된 객체의 값을 비교 후 변경된 데이터에 대한 update 쿼리문을 자동으로 생성하여 쓰기지연 SQL 저장소에 생성하여 저장합니다. 이러한 내부적인 로직때문에 수정할 때 persist나 update를 안해줘도 됩니다.
Entity 클래스를 만들고, 트랜잭션을 보내주면 log로 쿼리문을 볼 수 있습니다. 위와 같은 쿼리문이 로그에 출력된다면 정상적으로 jpa가 작동하는 것 입니다. 쿼리문이 로그로 출력이 되는 이유는 persistence.xml 코드에서 옵션 첫번째 줄에 show_sql의 value값을 true로 주었기 때문입니다. format_sql은 로그를 위와같이 가독성이 좋게 출력을 해줍니다. use_sql_comments는 주석된 부분을 로그로 보여줄 것인지 아닌지를 설정해줍니다. h2 콘솔로 확인해보면 입력했던 데이터가 저장된 것을 확인할 수 있습니다.
트랜잭션 정석코드로 변경했습니다. 트랜잭션을 보냈을 때, 에러가 발생할 시 rollback하고 정상적으로 종료되면 EntityManager와 EntityManagerFactory를 종료합니다. 지금은 jpa 정석 코드를 보여주기 위해 적었지만 실제로는 spring 위의 코드가 다 없어지고 em.persist(member)만 적으면 작업을 완료할 수 있습니다.
JPA 조회
조회는 위와 같이 find 메소드를 통해 찾을 수 있습니다. 파라미터로 엔티티 클래스명, PK를 넘겨주면 DB에 저장한 데이터를 가져올 수 있습니다. 가져온 Entity의 값을 출력해보면 이전에 넣었던 데이터가 출력되는 것을 볼 수 있습니다.
JPA 삭제
삭제는 위처럼 remove에 찾은 Entity 클래스 필드를 넣어주면 됩니다.
JPA 수정 (신기함)
수정이 진짜 신기합니다. 마치 자바 컬렉션을 사용하는 것처럼 사용만하면 수정이됩니다. 단지, setter를 활용해 데이터를 수정만해준 뒤 트랜잭션만 보내면 JPA가 자동으로 DB 데이터를 수정해줍니다. 이것이 가능한 이유는 JPA를 통해 Entity를 가져오면 해당 Entity는 JPA가 관리하기 때문입니다. 그리고 JPA가 변경이 되는지 안되는지 트랜잭션을 commit하는 시점에 체크합니다. 그 후 바뀐 데이터가 있을 시 업데이터 쿼리를 작성하여 DB에 날려줍니다.
주의
EntityManagerFactory는 하나만 생성해서 애플리케이션 전체에서 공유
웹 애플리케이션 서비스를 실행한다고하면 EntityManagerFactory는 DB 당 하나만 생성한다고 보면 됩니다.
EntityManager는 쓰레드간에 공유 X (사용하고 버려야 한다.)
고객의 요청에 따라서 사용한 뒤 close, 사용한 뒤 close 반복한다고 보면 됩니다.
이유는 후에 이해하면 정리하도록!
JPA의 모든 데이터 변경은 트랙잭션 안에서 실행
정말 중요합니다!
RDB 데이터 변경은 트랜잭션안에서 할 수 있도록 설계가 되어있기 때문입니다.
JPQL 소개
가장 단순한 조회 방법
EntityManager.find(Entity 클래스명, PK 값)
객체 그래프 탐색 (a.getB().getC())
나이가 18살 이상인 회원을 모두 검색하고 싶다면?
JPQL을 사용해야 합니다.
현업에서의 개발고민은 여러 테이블이 정말 많은데 필요하면 조인도해야하고, 원하는 데이터를 최적화 해서 가져와야하고, 필요하면 통계성 쿼리도 날려야하고 이러한 작업들을 JPA에서는 JPQL로 도와준다고 보면 됩니다. 쿼리를 어떻게 쓰지라는 고민이 드실텐데 JPA에서는 대안이 있다고 합니다.
실습 - JPQL 소개
JPQL로 전체 회원 탐색
JPQL로 ID가 2 이상인 회원만 검색
쿼리문에 where m.id =
JPQL로 이름이 같은 회원만 검색
JPQL에 대해 자세한 내용은 객체지향 쿼리에서 학습
JPQL로 전체 회원 탐색
log를 잘 보면 실제 전송한 쿼리랑 다르게 모든 필드를 나열한 것을 볼 수 있습니다.
JPQL
JPA를 사용하면 엔티티 객체 중심으로 개발할 수 있다.
문제는 검색 쿼리
검색을 할 때도 테이블이 아닌 엔티티 객체로 탐색
모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요
JPQL은 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공
SQL을 추상화했기 때문에 특정 데이터베이스에 의존 X
SQL 문법과 유사, SELECT FROM, GROUP BY, WHERE, HAVING, JOIN 지원
왜?? 라는 질문에 대답하기 위해선 해당 분야에 많은 지식이 있어야지 대답을 해줄 수 있습니다.' 앞으로 스스로에게 왜?? 라는 질문을 던지며 정리를 하려고합니다. 왜?? 라는 질문은 강의와 책을 보며 찾은 해답을 정리할 것이고, 아직 해답을 못찾은 왜?라는 질문에 대한 대답은 후에 정리하도록 하겠습니다.
JPA 왜 공부해야 하는가?
1. 현재 JPA를 공부하는 이유는 점유율입니다. JPA, Mybatis 등 중에서 세계적으로 앞도적인 점유율을 보여주는 JPA를 사용할 줄 알고, 이해하고 있어야지 취업에 도움이 될 것이라고 생각하기 때문입니다.
2. 무한 반복, 지루한 코드를 적어야하는 SQL의 문제점을 보완해주기 때문입니다. (간단하게 쿼리문 작성이 필요하지 않습니다.) 이에따라서, 개발 시간이 줄어듬으로써 설계나 테스트 등에 시간을 더 사용할 수 있습니다.
3. 시간을 줄여준다고 성능이 입증되지 않은 것도 아닙니다. 현재 조 단위의 거래 금액이 발생하는 다양한 서비스에서 사용하고 있으며 문제없이 사용되기 때문에 문제없다고 생각했습니다.
SQL 중심적인 개발의 문제점
무한 반복, 지루한 코드를 적어야하는 예를 한번 들어봅시다. JPA가 없는 SQL만으로 작성된 아래의 코드로 작성된 프로젝트가 있다고 가정해봅시다.
여기서, 기획자에 요청에 의해서 객체에 번호 필드를 추가해야되는 상황이 주어졌습니다. 객체에 tel이란 필드를 생성하면 끝나는 것이 아닌 모든 쿼리문에 tel을 추가해야되는 상황이 발생합니다.
결국, 관계형 데이터베이스를 사용하는 환경에서는 SQL에 의존적인 개발을 피하기 어렵습니다.
그리고 단순한 SQL 문을 작성하는 문제를 넘어서서 또 하나의 문제점이 있습니다. 바로 패러다임의 불일치 입니다.
패러다임의 불일치
객체 vs 관계형 데이터베이스
관계형 데이터베이스의 사상과 객체 지향 사상이 다릅니다.
객체 지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공합니다. 반면, 관계형 데이터베이스는 데이터를 잘 정규화해서 보관하는 것을 목표로합니다.
하지만 생각을 뒤집어 봅시다. 먼저 객체를 잘 정규화해서 저장한다고 봅시다. 선택지는 RDB, NoSQL, File 등 여러가지가 있습니다. 여기서 데이터를 저장하는 다양한 방법 중 현실적인 대안은 RDB입니다. 이유는 데이터가 백만건 있다고 할 때 파일에 집어 넣는 경우는 검색이 불가능하고 백만건 모두를 Object를 만든 뒤 검색하기에는 답이 없기 때문입니다. NoSQL도 대안이 될 수 있기도 하지만 아직까지 Main은 RDB를 쓰기 때문에 힘들다고 합니다. 그렇기 때문에 RDB를 사용해야합니다.
결론적으로, RDB를 사용함으로써 객체를 SQL로 바꿔야하기 때문에 SQL은 필수로 사용해야 합니다. 이러한 작업은 개발자가 해야합니다.
객체와 관계형 데이터베이스의 차이
상속
객체 : 상속 관계 O
RDB : 상속 관계 X
연관관계
객체 : 레퍼런스 ex) ArrayList에서의 get
RDB : PK, FK
데이터 타입
데이터 식별 방법
상속
위와 같은 상속 관계가 있다고 가정해봅시다.
삽입
객체 분해
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
insert의 경우 3번의 작업을 거치긴하지만 가능합니다.
조회
각각의 테이블에 따른 조인 SQL 작성 ..
각각의 객체 생성 ..
상상만 해도 복잡합니다.
그렇기때문에 DB에 저장할 객체에는 상속 관계를 쓰지 않습니다.
하지만, DB가 아닌 자바 컬렉션에 저장한다면 어떻게 될까요??
삽입
list.add(album);
조회
Album album = list.get(albumId);
// 부모 타입으로 조회 후 다형성 활용
Item item = list.get(albumID);
자바 컬렉션에 넣으면 심플해지지만, RDB에 넣고 빼는 순간 중간에 SQL 매핑 작업을 개발자가 한땀한땀 해야합니다.
연관관계
객체는 참조를 사용 : member.getTeam()
테이블은 외래 키를 사용 : JOIN ON M.TEAM_ID = T.TEAM_ID
객체의 경우 Member에서 Team을 찾을 수 있고, Team에서 Member를 찾을 순 없습니다. 하지만 테이블의 경우 MEMBER에서 TEAM을 join할 수 있고, TEAM에서 MEMBER를 join할 수 도 있습니다.
객체를 테이블에 맞추어 모델링
class Member {
String id; // MEMBER_ID 컬럼 사용
Long teamId; // TEAM_ID FK 컬럼 사용 // **
String username;// USERNAME 컬럼 사용
}
class Team {
Long id; // TEAM_ID PK 사용
String name; // NAME 컬럼 사용
}
테이블에 맞춘 객체 저장
하지만 이는 객체다운 모델링 같지 않습니다. Member 클래스에 있는 teamId의 타입이 Team이 아닌 Long으로 되어있기 때문입니다.
객체다운 모델링
class Member {
String id; // MEMBER_ID 컬럼 사용
Team teamId; // 참조로 연관관계를 맺는다. // **
String username;// USERNAME 컬럼 사용
Team getTeam() {
return team;
}
}
class Team {
Long id; // TEAM_ID PK 사용
String name; // NAME 컬럼 사용
}
객체 모델링 저장
이렇게 설계하면 정상적으로 저장이 가능합니다. 하지만 조회할 때 문제가 생깁니다.
객체 모델링 조회
위와 같이 주석처리 된 부분의 코드가 상당할 것으로 예상됩니다. 결론적으로 매우 번거롭습니다.
객체 모델링, 자바 컬렉션에 관리
훨씬 편리해진 것을 볼 수 있습니다.
객체 그래프 탐색
객체는 자유롭게 객체 그래프를 탐색할 수 있어야 합니다. 아래의 그래프를 예시로 들어 봅시다.
여기서 자유롭게 그래프를 탐색한다는 것은 자바에서 get을 통해 모든 객체로 갈 수 있다는 것을 의미합니다.
처음 실행하는 SQL에 따라 탐색 범위 결정
이처럼 SELECT로 MEMBER와 TEAM을 가져오면 getTeam은 가능하지만, getOrder은 불가능합니다. 이것을 해결하기 위해선 일일이 코드를 다 살펴봐야하는 문제가 발생하게 됩니다. 즉, 엔티티에 신뢰할수 없다는 것 입니다.
엔티티 신뢰 문제
위에 코드가 있다고 가정합시다. Member를 받아와서 getTeam(), getOrder().getDelivery()를 수행하려 합니다. 하지만 엔티티를 모두 가져올 수 있을지는 장담할 수 없습니다. 이를 알아보기 위해선 Member를 가져올 때 TEAM, ORDER, DELIVERY 데이터를 모두 긁어 오는지를 확인해야 합니다.
모든 객체를 미리 로딩할 수는 없다.
그렇다면 모든 객체를 미리 모두 로딩해놓으면 되는 것 아닌가라는 의문점이 생깁니다. 하지만 사실상 현업에서 사전에 미리 로딩하는 작업을 하게되면 서비스를 이용하기 직전에 불필요한 데이터까지 조회를 하기때문에 너무 오랜 시간을 기다릴 수 있어 서비스적인 측면에서 좋지 못합니다. (개인적인 생각입니다.) 그렇기때문에 위와같이 테이블을 조회하는 모든 경우의 수를 메소드화 시키는 것이 좋지만 이또한 코드가 지저분해지고 지루한 코드를 오랜시간 작성해야하는 번거로움이 있습니다.
이렇듯 SQL을 직접 다루게 되면 계층형 아키텍처에서 진정한 의미의 계층 분할이 어렵습니다. (진정한 게층 분할은 뭘까??)
비교하기
SQL에서 조회
member1과 member2가 식별자는 똑같지만 다른 이유는 다른 쿼리문으로 받아온 값을 new Member를 통해 반환하기 때문에 당연히 다릅니다. 그렇다면 자바 컬렉션에서는 어떨까요?
자바 컬렉션에서 조회
이것은 당연하게 같은 참조값으로 조회하기때문에 같은 Member를 반환합니다.
이처럼 자바 컬렉션에서 객체를 다룰 때와 SQL(RDB)에서 객체를 다룰 때와 중간에 많은 미스매치가 나는 것을 볼 수 있습니다. 그렇기때문에 RDB를 객체답게 모델링 할수록 매핑 작업만 늘어나게 됩니다.
이러한 무한 반복, 지루한 코드를 계속해서 작성하던 선배 개발자분들은 "객체를 자바 컬렉션에 저장 하듯이 DB에 저장"하고 싶어집니다. 이러한 고민의 결과가 바로 JPA이고, 공부를 해야하는 이유 입니다.
JPA 소개
JPA란?
Java Persistence API의 약자로 자바 진영의 ORM 기술 표준입니다.
ORM이란?
Object-Relational Mapping (객체 관계 매핑)의 약자로 객체는 객체대로 설계, 관계형 데이터베이스는 관계형 데이터베이스대로 설계하는 것을 의미합니다. 각각 설계된 객체와 RDB를 ORM 프레임워크가 중간에서 매핑합니다.
JPA 동작
JPA는 JAVA 애플리케이션과 JDBC 사이에서 동작합니다. 그렇기때문에 개발자가 직접 JDBC API를 쓰는 것이 아닌 JPA에게 명령하면 JPA가 JDBC API를 사용해서 SQL을 호출하고 만들어서 보내고 결과를 받는 등의 동작을 하게 됩니다.
JPA 동작 - 저장
예를들어, MemberDAO에서 객체를 저장하고 싶다고 가정해봅시다. 과거에는 JDBC API나 JDBC Template, Mybatis를 썼다면 JPA를 사용하게 될 경우에는 JPA에게 Member 객체를 넘깁니다. 그렇게되면 JPA가 Member 객체를 분석하고 적절한 INSERT SQL을 생성하고 JDBC API를 사용해서 쿼리를 DB에 보냅니다. 여기서 JPA의 장점은 쿼리를 개발자가 작성하는 것이 아닌 JPA가 작성해줍니다. 그렇기때문에 패러다임의 불일치가 해결됩니다. (아직까지는 어떻게 패러다임의 불일치가 해결되는지 이해가지 않지만 뒤에서 해결해준다고합니다.)
JPA 동작 - 조회
조회할 때도 마찬가지로 JPA에게 PK값만 넘기면 JPA가 알아서 쿼리문을 작성하고 JDBC API를 사용해서 DB를 보내고 결과를 받은 다음에 ResultSet 결과를 객체에 다 매핑해줍니다. 그렇기때문에 코드한줄로 조회기능을 애플리케이션 단에서 가능하게 됩니다.
JPA 역사
해당 역사는 기본편에서 한번 보았던 내용인 것 같아 따로 정리하지않고 링크를 아래 추가했습니다.
JPA는 표준 명세
JPA는 인터페이스의 모음
JPA 2.1 표준 명세를 구현한 3가지 구현체
하이버네이트(대표적), EclipseLink, DataNucleus
JPA를 왜 사용해야 하는가?
SQL 중심적인 개발에서 객체 중심으로 개발
생산성
유지보수
패러다임의 불일치 해결
성능
데이터 접근 추상화와 벤더 독립성
표준
생산성 - JPA와 CRUD
저장
jpa.persist(member)
조회
Member member = jpa.find(memberId)
수정 (fantastic)
member.setName("updateName")
삭제
jpa.remove(member)
코드가 모두 만들어져있고 가져다 쓰기만해서 기존 CRUD를 하기위해 SQL 문을 작성하던 번거로움이 사라졌습니다.
유지보수 - 기존 : 필드 변경시 모든 SQL 수정
위에서 느꼈던 기획자에 요청에 의해 필드가 변경될 때 모든 SQL 문을 수정해야하는 번거로움이 있었습니다. 하지만 아래와 같이 JPA에서는 JAVA 애플리케이션 단에서 컬럼만 추가하면 됩니다. 따라서 유지보수 측면에서 훨씬 우월하다고 볼 수 있습니다.
JPA와 패러다임의 불일치 해결
JPA와 상속
JPA와 연관관계
JPA와 객체 그래프 탐색
JPA와 비교하기
JPA와 상속
이전에 보았던 상속 관계입니다.
JPA와 상속 - 저장
이전에 SQL 문을 작성했던 번거로움이 사라진 것을 볼 수 있습니다. 또한, 상속관계에 있는 album을 보고 알아서 ITEM의 INSERT 쿼리문도 작성해주는 것을 볼 수 있습니다.
JPA와 상속 - 조회
기가 막힙니다. JPA에 앨범의 PK 값을 넘기면 JPA에서 ITEM과 ALBUM을 조인해서 데이터를 가져오는 것을 볼 수 있습니다. 패러다임이 맞지 않는 부분을 JPA가 해결해줍니다.
JPA와 연관관계, 객체 그래프 탐색
member에 team을 set한 뒤 member를 jpa에 보내 DB에 저장합니다. 후에 조회하여 member로 team을 조회하면 가져올 수 있습니다. 마치 자바 컬렉션에 넣었던 것 처럼!!
신뢰할 수 있는 엔티티, 계층
JPA를 통해서 member 객체를 가져온거면 객체 그래프를 자유롭게 모두 다 탐색할 수 있습니다. JPA는 지연로딩이라는 기능덕분에 member.getTeam()이나 member.getOrder()를 해서 실제 객체를 사용해서 조회하는 시점에 SQL을 보내 채워주는 기능이 있습니다. 그렇기때문에 자유롭게 탐색할 수 있습니다. 그렇기때문에 신뢰할 수 있는 엔티티가 될 수 있습니다.
JPA와 비교하기
동일한 트랙잭션에서 조회한 엔티티는 같음을 보장합니다. 이것이 가능한 이유는 강의를 보다보면 이해할 수 있다고 합니다. (아직 이해 안됨)
JPA의 성능 최적화 기능
1차 캐시와 동일성(identity) 보장
트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
지연 로딩 (Lazy Loading)
JPA를 사용하면 성능이 떨어질 수도 있다고 생각할 것 입니다. 이렇게 생각하는 이유는 제 생각으론 기존에는 JAVA 애플리케이션에서 JDBC API로 바로 사용했지만, JPA를 사용하면 중간에 JPA를 거치기때문에 구조가 한단계 더 깊어졌기 때문입니다.
계층 사이에 중간 계층이 있으면 항상 할 수 있는 것이 있습니다. 바로 모아서 쏘는 버퍼링과 읽을 때 캐싱하는 것을 할 수 있습니다. (마치 CPU와 메모리 사이 캐시기능) JAVA 애플리케이션과 JDBC API의 중간계층으로 JPA가 있기 때문에 SQL을 사용하는 것보다 이러한 부분들을 최적화 할 수 있습니다.
1차 캐시와 동일성 보장
같은 트랜잭션 안에서는 같은 엔티티를 반환 - 약간의 조회 성능 향상
DB Isolation Level이 Read Commit이어도 애플리케이션에서 Repeatable Read 보장 (DB 지식이 많이 있어야하기 때문에 나중에 이해하고 이해해야할 부분, 현재는 알아두기만 할 것)
아까 조회했던 코드입니다. 첫번째 조회는 SQL을 통해 조회하고 두번째는 캐시를 통해 데이터를 가져옵니다. 그렇기때문에 m1과 m2는 같고 조회 성능을 향상시킬 수 있습니다. 실제로 캐싱기능을 지원해주는 것이 아닌 트랜잭션을 통해 제공해주는 기능이기 때문에 굉장히 짧은 시간에만 사용이 가능해서 실무에서 큰 도움은 되지 않는다고 합니다.
트랜잭션을 지원하는 쓰기 지연 - INSERT
트랜잭션을 커밋할 때까지 INSERT SQL을 모음
JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송
JDBC BATCH 기능인 한번에 여러 SQL 기능을 사용해서 한번에 SQL 전송을 하는 기능이 있습니다. JPA를 사용하지 않는 경우에는 상당히 복잡하다고 하는데 JPA를 사용하면 위와같이 간단하게 사용할 수 있습니다.
트랜잭션을 지원하는 쓰기 지연 - UPDATE
UPDATE, DELETE로 인한 로우(ROW)락 시간 최소화
트랜잭션 커밋 시 UPDATE, DELETE SQL 실행하고, 바로 커밋
지연 로딩과 즉시 로딩
지연 로딩 : 객체가 실제 사용될 때 로딩
즉시 로딩 : JOIN SQL로 한번에 연관된 객체까지 미리 조회
JPA에서 중요한 부분 중 하나입니다. 위 그림과 같이 지연 로딩은 객체가 사용될 때 로딩을 합니다. 지연 로딩의 많은 쿼리가 발생하는 단점을 보완한 기능인데 Member 조회해서 사용할 때 team을 거의 같이 사용한다고 가정될 때 member를 로딩할 때 team도 같이 조회해서 가져오는 것을 말합니다. 옵션을 통해 지연 로딩과 즉시 로딩을 껐다 켤 수 있다고 합니다. 김영한님은 지연 로딩으로 코드를 다 작성한 후 최적화할 때 즉시 로딩이 필요한 부분만 최적화를 진행한다고 합니다.
마지막으로
ORM이라는 기술은 객체와 RDB 두 기둥위에 있는 기술입니다. JPA만 잘 안다고해서 잘 할 수 있는 것이 아니고 RDB만 잘 안다고해서 잘할 수 있는 것이 아닙니다. JPA는 객체와 RDB 두 가지 사이에서 밸런스를 잘 맞춰야합니다. 한 가지에 편향하지 않고 JPA를 공부하더라도 RDB도 꾸준하게 같이 공부해야 한다. 꼭! 명심해!
데이터는 캐시해도 되지만, 항상 원(origin) 서버에 검증하고 사용 (원 서버란 캐시 서버, 프록시 서버가 아닌 진짜 서버)
Cache-Control : no-store
데이터에 민감한 정보가 있으므로 저장하면 안됨 (메모리에서 사용하고 최대한 빨리 삭제)
Pragma
캐시 제어 (하위 호환)
Pragma : no-cache
HTTP 1.0 하위호환
Expires
캐시 만료일 지정 (하위 호환)
expires : Mon, 01 Jan 1990 00:00:00 GMT
캐시 만료일을 정확한 날짜로 지정
HTTP 1.0 부터 사용
지금은 더 유연한 Cache-Control:max-age 권장
Cache-Control:max-age와 함께 사용하면 Expires는 무시
프록시 캐시
원 서버 직접 접근
origin 서버
이렇게 클라이언트마다 원 서버로 접근하게되면 500ms를 다운로드 받을 때마다 0.5초를 소모하게 됩니다. 이러한 시간을 단축시키고자 프록시 서버라는 것이 등장하게 됩니다.
프록시 캐시 도입
첫 번째 요청
이러한 프록시 캐시 서버 덕분에 유튜브, 넷플릭스 같은 서비스를 모든 나라에서 빨리 볼 수 있는 것 입니다. (이를 증명하는 것은 한국서버에서 한국인이 자주 보는 동영상은 로딩 속도가 되게 빠르지만, 한국인이 잘 보지 않는 외국 기술 동영상들을 보면 로딩 속도가 매우 느립니다.)
Cache-Control
캐시 지시어(directives) - 기타
Cache-Control : public
응답이 public 캐시에 저장되어도 됨
Cache-Control : private
응답이 해당 사용자만을 위한 것임, private 캐시에 저장해야 함(기본값) (공용으로 사용되는 이미지는 public에 저장 되도 되지만, 개인 정보같은 경우는 public에 저장되면 안되기 때문)
Cache-Control : s-maxage
프록시 캐시에만 적용되는 max-age
Age : 60 (HTTP 헤더)
오리진 서버에서 응답 후 프록시 캐시내에 머문 시간(초)
캐시 무효화
Cache-Control
확실한 캐시 무효화 응답
Cache-Control을 적지 않아도 많이 사용 되는 경우 임의로 캐시에 등록 될 수 있기 때문에, 절대 캐시가 되면 안되는 페이지는 아래와 같은 작업을 해주어야 합니다.
3번의 경우 프록시 캐시에서 원 서버의 장애로 장애를 웹 브라우저에 보내기보다는 이전 데이터라도 쓰라며 200 OK를 보내는 경우가 있다고 합니다. 이것이 no-cache의 정책입니다.
must-revalidate
위의 no-cache인 캐시 지시어는 프록시 캐시에서 원 서버로 접근하는 네트워크가 단절되도 웹 브라우저한테는 이전의 데이터로라도 사용하라며 200 OK를 보내주지만, must-revalidate의 경우는 원 서버와 네트워크가 되지 않으면 웹 브라우저에게 504 Gateway Timeout의 응답을 줍니다. 이것이 no-cache와 must-revalidate의 차이 입니다.
보낼 message body를 gzip 등을 이용해 압축해서 전송하는 것 참고) HTTP HEAD 부분에 Contetn-Encoding을 적어주어야함.
분할 전송
보낼 데이터를 byte 단위로 끊어서 전송하는 것 참고) HTTP HEAD에 Transfer-Encoding = chunked를 추가해줘야함. 참고) Content-Length를 넣으면 안된다. (총 몇 바이트인지 예상이 안되기때문) 아래의 Body로 예시를 들면 5byte Hello 먼저 전송 5byte World 전송 0Byte 전송 용량이 되게 큰 경우에 이렇게 나누어서 전송함
범위 전송
이미지를 받는데, 절반정도 받고 나머지 절반을 받는 형식 이유 : 큰 용량의 데이터를 받는 경우 중간에 받지못하면 많은 양의 데이터를 다시 처음부터 받아야하기 때문
일반 정보
From : 유저 에이전트의 이메일 정보
Referer : 이전 웹 페이지 주소
User-Agent : 유저 에이전트 애플리케이션 정보
Server : 요청을 처리하는 오리진 서버의 소프트웨어 정보
Data : 메시지가 생성된 날짜
From
유저 에이전트의 이메일 정보
일반적으로 잘 사용되지 않음
검색 엔진 같은 곳에서, 주로 사용
요청에서 사용
Feferer
이전 웹 페이지 주소
현재 요청된 페이지의 이전 웹 페이지 주소
A -> B로 이동하는 경우 B를 요청할 때 Referer: A를 포함해서 요청
Referer를 사용해서 유입 경로 분석 가능 (내 사이트를 어디 사이트를 통해 들어왔는지 데이터 분석할 때 많이 사용)
요청에서 사용
참고 : referer는 단어 referrer의 오타 => ...??ㅋㅋㅋ그렇다고합니다...
실제 구글에서 hello를 검색한 뒤, 나무위키를 들어가서 개발자 도구에서 Request Headers를 보시면 referer에 이전 페이지의 주소가 적혀있는 것을 볼 수 있습니다.
User-Agent
유저 에이전트 애플리케이션 정보
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/ 537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36
클라이언트의 애플리케이션 정보 (웹 브라우저 정보, 등등)
통계 정보
어떤 종류의 브라우저에서 장애가 발생하는지 파악 가능 (Chrome, Edge 등등..)
요청에서 사용
Server
요청을 처리하는 ORIGIN 서버의 소프트웨어 정보 (프록시, 캐시 서버를 제외한 진짜 나의 요청을 응답하는 서버 - ORIGIN 서버)
Server : Apache/2.2.22 (Debian)
server : nginx
응답에서 사용
Date
메시지가 발생한 날짜와 시간
Date : Tue, 15 Nov 1994 08:12:31 GMT
응답에서 사용
특별한 정보
HOST : 요청한 호스트 정보 (도메인)
Location : 페이지 리다이렉션
Allow : 허용 가능한 HTTP 메서드
Retry-After : 유저 에이전트가 다음 요청을 하기까지 기다려야 하는 시간
HOST
요청한 호스트 정보 (도메인)
요청에서 사용
필수
하나의 서버가 여러 도메인을 처리해야 할 때 (구글, 네이버 등등과 같은 경우)
하나의 IP 주소에 여러 도메인이 적용되어 있을 때
하나의 서버가 여러 도메인을 처리한다는 것은 가상호스트를 통해 여러 도메인을 한번에 처리할 수 있는 서버로 실제 애플리케이션이 여러개 구동될 수 있다고 합니다.
문제는 호스트가 없이 /hello만 온다면 아래와 같이 어떤 도메인에 /hello인지 모른다는 것 입니다.
그렇기 때문에 HOST를 적어주어야지 서버에서는 정확한 응답을 해줄 수 있습니다.
Location
페이지 리다이렉션
웹 브라우저는 3xx 응답의 결과에 Location 헤더가 있으면, Location 위치로 자동 이동(리다이렉트)
응답코드 3xx에서 설명
201 (Created) : Location 값은 요청에 의해 생성된 리소스 URI
3xx (Redirection) : Location 값은 요청을 자동으로 리다이렉션하기 위한 대상 리소스를 가리킴
Allow
허용 가능한 HTTP 메서드
허용하지 않는 HTTP 메서드로 접근하는 경우
405 (Method Not Allowd)에서 응답에 포함해야함
Allow : GET, HEAD, PUT
Retry-After
유저 에이전트가 다음 요청을 하기까지 기다려야 하는 시간
503 (Service Unavailable) : 서비스가 언제까지 불능인지 알려줄 수 있음
Retry-After : Fri, 31 Dec 1999 23:59:59 GMT (날짜 표기)
Retry-After : 120 (초단위 표기)
인증
Authorization : 클라이언트 인증 정보를 서버에 전달
WWW-Authenticate : 리소스 접근시 필요한 인증 방법 정의
Authorization
클라이언트 인증 정보를 서버에 전달
Authorization : Basic xxxxxxxxxxxxxxxxxxx
Oauth 인증 등등 value에 들어가는 입력이 다름
WWW-Authenticate
리소스 접근시 필요한 인증 방법 정의
리소스 접근시 필요한 인증 방법 정의
401 Unauthorized 응답과 함께 사용
WWW-Authenticate : Newauth realm= apps",type=1, title="Login to \"apps\"", Basic realm="simple"
인증을 하려면 "New~simple"과 정보를 참고해서 제대로된 인증 정보를 만들라고 응답
쿠키
Set-Cookie : 서버에서 클라이언트로 쿠키 전달 (응답)
Cookie : 클라이언트가 서버에서 받은 쿠키를 저장하고, HTTP 요청시 서버로 전달
쿠키 미사용
처음 welcome 페이지 접근
로그인
로그인 이후 welcome 페이지 접근
HTTP는 메시지를 다 보내고나면 연결을 끊기 때문에 쿠키를 사용하지 않으면 서버에서 이전에 로그인을 했던 사용자인지 처음온 사람인지 알 수 있는 방법이 없습니다. (HTTP가 Stateless이기 때문)
4xx (Client Error) : 클라이언트 오류, 잘못된 문법등으로 서버가 요청을 수행할 수 없음
5xx (Server Error) : 서버 오류, 서버가 정상 요청을 처리하지 못함
2xx (성공)
클라이언트의 요청을 성공적으로 처리
200 OK
요청 성공
201 Created
요청이 성공해서 새로운 시소스가 생성
응답 헤더에 "Location : (새로생성된 리소스 URI)"를 포함해서 응답
202 Accepted
요청이 접수되었으나 처리가 완료되지 않는 경우
배치 처리 같은 곳에서 사용
예) 요청 접수 후 1시간 뒤에 배치 프로세스가 요청을 처리함
204 No Content
서버가 요청을 성공적으로 수행했지만, 응답 페이로드 본문에 보낼 데이터가 없음
예) 웹 문서 편집기에서 save 버튼 (클라이언트 입장에서 서버에서 데이터를 보내줄 필요가 없음)
결과 내용이 없는 204 메시지만으로 성공을 인식해도 되는 경우
3xx (리다이렉션1)
요청을 완료하기 위해 클라이언트 웹 브라우저의 추가 조치 필요
300 Multiple Choices
301 Moved Permanently
302 Found
303 See Other
304 Not Modified
307 Temporary Redirect
308 Permanent Redirect
리다이렉션 이해
웹 브라우저는 3xx 응답의 결과에 Location 헤더가 있으면, Location 위치로 자동 이동(리다이렉트)
기존 event 시에 /event로 접근하였는데 새로운 이벤트는 /new-event로 접근하는 경우 301 코드와 새로바뀐 path를 Location에 포함해서 응답을 보냅니다. 그렇게되면 웹 브라우저가 스스로 해당 url로 바꾼뒤 새롭게 검색합니다. (자동 리다이렉트)
리다이렉션 종류
영구 리다이렉션 - 특정 리소스의 URI가 영구적으로 이동
예) /members -> /users
예) /event -> /new-event
일시 리다이렉션 - 일시적인 변경
주문 완료 후 주문 내역 화면으로 이동
PRG 패턴 : Post/Redirect/Get
특수 리다이렉션
결과 대신 캐시를 사용
캐시가 만료된 것 같아 캐시와 관련된 정보를 서버에 제공했는데, 서버에서 캐시사용해도 되니깐 계속 사용하라고 응답하는 경우
영구 리다이렉션 - 301, 308
리소스의 URI가 영구적으로 이동
원래의 URL을 사용하지 않고, 검색 엔진 등에서도 변경 인지
301 Moved Permanently
리다이렉트시 요청 메서드가 GET으로 변하고, 본문이 제거될 수 있음
POST로 요청을 했을 시에는 다시 처음부터 입력을 해야됩니다. (POST 요청 메서드가 GET 메서드로 바뀌었기 때문)
308 Permanent Redirect
301과 기능은 같음
리다이렉트시 요청 메서드와 본문 유지(처음 POST를 보내면 리다이렉트도 POST 유지)
일시적인 리다이렉션 - 302, 307, 303
리소스의 URI가 일시적으로 변경
따라서 검색 엔진 등에서 URL을 변경하면 안됨
302 Found
리다이렉트시 요청 메서드가 GET으로 변하고, 본문이 제거될 수 있음
307 Temporary Redirect
302와 기능은 같음
리다이렉트시 요청 메서드와 본문 유지(요청 메서드를 변경하면 안된다.)
303 See Other
302와 기능은 같음
리다이렉트시 요청 메서드가 GET으로 변경
PRG : Post / Redirect / Get
일시적인 리다이렉션 - 예시
POST로 주문후에 웹 브라우저를 새로고침하면?
새로고침은 다시 요청
중복 주문이 될 수 있다.
원칙적으로는 서버에서 막아줘야합니다. 그래도 클라이언트에서도 하나정도는 막아주면 좋습니다. 위의 문제를 해결하기 위해 PRG라는 패턴을 주로 씁니다.
POST로 주문후에 새로 고침으로 인한 중복 주문 방지
POST로 주문후에 주문 결과 화면을 GET 메서드로 리다이렉트
새로고침해도 결과 화면을 GET으로 조회
중복 주문 대신에 결과 화면만 GET으로 다시 요청
3번의 응답 상태 코드는 302, 303 모두 가능합니다.
이렇게되면 사용성이 좋습니다. 사용자의 입장에서 실수로 새로고침해도 결과화면이 깔끔하게 보이고 서버입장에서는 오류가 줄어들기 때문입니다.
위의 경우 303, 307을 권장하지만 현실적으로 이미 많은 애플리케이션 라이브러리들이 302를 기본값으로 사용하고 있다고 합니다. 위처럼 자동 리다이렉션시에 GET으로 변해도 되면 302를 사용해도 큰 문제는 없습니다.
이미지 경로만 URI에 넣어서 GET 요청을하면 서버에서는 해당 경로에 있는 데이터를 클라이언트에게 반환해줍니다. 정적 데이터를 조회할 때는 파라미터가 필요하지 않습니다.
정적 데이터 조회 - 정리
이미지, 정적 테스트 문서
조회는 GET 사용
정적 데이터는 일반적으로 쿼리 파라미터 없이 리소스 경로로 단순하게 조회 가능
동적 데이터 조회 - 쿼리 파라미터 사용
동적 데이터를 조회할 때는 데이터를 서버측에 넘겨줘야합니다. 이때, 쿼리 파라미터를 URI에 추가하여 사용합니다.
동적 데이터 조회 - 정리
주로 검색, 게시판 목록에서 정렬 필터 (검색어)
조회 조건을 줄여주는 필터, 조회 결과를 정렬하는 정렬 조건에 주로 사용
조회는 GET 사용
GET은 쿼리 파라미터를 사용해서 데이터를 전달
HTML Form 데이터 전송 - POST 전송 (저장)
form tag를 사용해 웹브라우저가 submit 버튼을 클라이언트에서 누르게 되면 form의 데이터를 읽어서 http 메시지를 생성해줍니다.
username=kim&age=20
쿼리 파라미터와 거의 유사하게 key=value 스타일로 데이터를 만들고 http body에 넣어준 모습입니다.
만약, 위의 method를 "GET"으로 바꾸면 어떻게 될까요??
GET이니까 body에 데이터가 들어가지 않고 쿼리 파라미터처럼 넣어서 서버로 전달합니다.
HTML Form 데이터 전송 - multipart/form-data
multipart/form-data는 파일 같은 것을 전송할 때 쓰는 인코딩 타입입니다. 이럴 때는 message body에 넣는 방식을 사용해야합니다. message Body에 boundary를 통해 각각의 key의 value가 어떤 값인지 나타낼 수 있또록 자동으로 생성해줍니다.
HTML Form 데이터 전송
HTML Form submit시 POST 전송
예) 회원 가입, 상품 주문, 데이터 변경
Content-Type : application/x-www-form-urlencoded 사용
form의 내용을 메시지 바디를 통해서 전송(key=value, 쿼리 파라미터 형식)
전송 데이터를 url encoding 처리
예) abc김 -> abc%EAA%B9%80
HTML Form은 GET 전송도 가능
Content-Type: multipart/form-data
파일 업로드 같은 바이너리 데이터 전송시 사용
다른 종류의 여러 파일과 폼의 내용 함께 전송 가능 (그래서 이름이 multipart)
참고: HTML Form 전송은 GET, POST만 지원
HTTP API 데이터 전송
보통 라이브러리가 있는데 사용해서 넘기면 됩니다.
HTTP API 데이터 전송 - 정리
서버 to 서버
백엔드 시스템 통신
앱 클라이언트
안드로이드, 아이폰
웹 클라이언트
HTML에서 Form 전송 대신 자바 스크립트를 통한 통신에 사용 (AJAX)
예) React, VueJs 같은 웹 클라이언트와 API 통신
POST, PUT, PATCH : 메시지 바디를 통해 데이터 전송
GET: 조회, 쿼리 파라미터로 데이터 전달
Content-Type: application/json을 주로 사용 (사실상 표준)
TEXT, XML, JSON 등등
HTTP API 설계 예시
HTTP API - 컬렉션
POST 기반 등록
예) 회원 관리 API 제공
HTTP API - 스토어
PUT 기반 등록
예) 정적 컨텐츠 관리, 원격 파일 관리
HTML FORM 사용
웹 페이지 회원 관리
GET, POST만 지원
회원 관리 시스템 API 설계 - POST 기반 등록
회원 목록 /members -> GET
회원 등록 /members -> POST
회원 조회 /members/{id} -> GET
회원 수정 /members/{id} -> PATCH, PUT, POST
회원 삭제 /members/{id} -> DELETE
회원 관리 시스템 POST - 신규 자원 등록 특징 (서버에서 리소스 URI를 결정하고 만들어줌)
클라이언트는 등록될 리소스의 URI를 모릅니다.
회원 등록 /members -> POST
POST /members
서버가 새로 등록된 리소스 URI를 생성해줍니다.
HTTP/1.1 201 Created Location: /members/100
컬렉션(Collection)
서버가 관리하는 리소스 디렉토리
서버가 리소스의 URI를 생성하고 관리
여기서 컬렉션은 /members
파일 관리 시스템 API 설계 - PUT 기반 등록
파일 목록 /files -> GET
파일 조회/files/{filename} -> GET
파일 등록/files/{filename} -> PUT
파일 삭제/files/{filename} -> DELETE
파일 대량 등록 /files -> POST
파일 관리 시스템 PUT - 신규 자원 등록 특징
클라이언트가 리소스 URI를 알고 있어야 합니다.
파일 등록 /files/{filename} -> PUT
여기서 filename을 클라이언트가 알고 있어야합니다.
클라이언트가 직접 리소스의 URI를 지정합니다.
스토어(Store)
클라이언트가 관리하는 리소스 저장소
클라이언트가 리소스의 URI를 알고 관리
여기서 스토어는 /files
POST 등록과 PUT 등록 중 많이 사용하는 것은 POST기반의 컬렉션을 많이 사용합니다.
HTML FORM 사용
HTML FROM은 GET, POST만 지원
AJAX 같은 기술을 사용해서 해결 가능 -> 회원 API 참고
여기서는 순수 HTML, HTML FORM 이야기
GET, POST만 지원하므로 제약이 있음
회원 관리 시스템 API 설계 - HTML FORM 기반
회원 목록 /members -> GET
회원 등록 폼 /members/new -> GET
회원 등록 /members/new, /members -> POST
회원 조회 /members/{id} -> GET
회원 수정 폼 /members/{id}/edit -> GET
회원 수정 /members/{id}/edit, /members/{id} -> POST
회원 삭제 /members/{id}/delete -> POST
회원 등록(new), 수정(edit), 삭제(delete)의 경우 GET, POST만 사용할 수 있는 HTML FORM에서는컨트롤 URI(동사로된 URI)를 쓸 수 밖에 없습니다.