client는 server에 접속을 하기 위해서는 해당 요청이 유효한지 인증을 받는 절차를 가져야 한다. 아무 요청이나 다 받아주면 server에 문제가 생길 수 있기 때문이다. 그래서 Token이라는 이용권을 발급하여 client가 server에 접속할 수 있도록 한다. 비유를 하자면 놀이공원에 들어갈 때 쓰는 자유이용권을 생각하면 된다. Token 방식에는 2가지 방법이 있다. OAuth(Open Authorization)와 JWT(JSON Wen Token) 이다. 여기서는 OAuth 2.0에 대해서 설명한다. 전자는 protocol, 후자는 standard에 들어간다. protocol과 standard의 차이점은 밑의 글을 참고해보시라.
<link 넣기>
# OAuth 란?
OAuth의 정의는 다음과 같다.
Internet user들이 password를 제공하지 않고 타 website 상의 user의 정보에 대해 website나 application의 접근 권한을 부여할 수 있는 공통적인 수단으로써 사용되는, 접근 위임을 위한 개방형 표준이다. - Wiki pedia
조금 더 쉽게 설명해 보자면, 우리가 주로 쓰는 카카오톡, 구글, 페이스북 등의 소셜 아이디로 로그인 하는 방식을 간편하게 사용할 수 있도록 도와주는 protocol이다. 이 밖에도 다른 외부 Web application에서 제공하는 기능들을 사용할 수 있고, password를 제공하지 않기 때문에 보안성이 높다는 장점 또한 있다. 단점은 복잡한 로직 과정이 있고 refresh, access token을 다 저장해야 해서 크기가 크다는 단점이 있다.
먼저 OAuth의 구성 요소들과 주요 용어를 살펴보자.
구성 요소 / 용어
설명
authentication
인증, 접근 자격이 있는지 검증하는 단계.
authorization
인가, resource에 접근할 권한을 부여하는 것. 인가가 완료되면 resource 접근 권한이 담긴 access token이 client에게 부여된다.
resource owner
web service를 이용하려는 유저, resource(개인정보)을 소유하는 자, 사용자.
authorization server
인증/인가를 수행하며 client의 접근 자격을 확인하고 권한을 부여하는 역할을 수행한다. resource owner는 이 server로 id, password를 넘겨 authorization code를 발급 받을 수 있다. client는 이 server로 authorization code를 넘겨 access token을 발급 받을 수 있다.
client
자사 또는 개인이 만든 application server. resource server에게 필요한 자원(보호된 자원)을 호스팅하는 관계.
resource server
resource owner의 보호된 자원을 호스팅하는 server. 소셜 로그인을 할 때, 해당 회사(Google, Facebook, Kakao, etc.)의 server를 말한다.
access token
resource owner에게 resource 접근 권한을 인가했음을 나타내는 만료 기간이 있는 자격증명(token) 보통 15~30분 정도의 만료 기간을 가진다.
refresh token
access token 만료시 이를 갱신하기 위한 용도로 사용하는 token. refresh token이 없다면 resource owner는 다시 로그인을 시도해야 한다. 하지만 refresh token이 있다면 이 token을 통해 access token을 재발급 받을 수 있다. 보통 30일 정도의 만료 기간을 가진다.
# OAuth 의 Protocol Flow
(A) client가 resource owner에게 authorization request를 한다. authorization request는 resource owner에게 직접 authorization request를 할 수 있다. 제일 바람직한 방식은 authorization server를 중개자로 두어 간접적으로 request를 하는 방식이다.
(B) client는 authorization grant를 받는다. resource owner의 authorization을 나타내는 자격 증명으로, 밑의 authorization Grant types 중 하나를 사용하거나, extension grant type을 사용한다.
(C) client는 autorization server로 authentication하고 access token request를 보낸다.
authorization grant types에는 총 4가지 방식이 있다. - 권한 부여 승인 코드 (Authorization Code) - 암묵적 승인 (Implicit) - 자원 소유자 자격증명 승인 (Resource Owner Password Credentials) - 클라이언트 자격증명 승인 (Client Credentials)
## Authorization Code
authorization code는 autorization server를 사용하여 가져온다. autorization code는 client와 resource owner 사이의 중개자 역할을 수행하게 된다.. resource owner에게 직접 권한을 요청하는 대신, client는 resource owner를 authorization server로 보내고, resource owner를 client에게 전달하기 전에 autorization code, autorization server가 인증을 한 뒤 authorization code를 사용하여 resource owner를 다시 client로 보낸다.
resource owner은 autorization server만 인증하므로, resource owner의 인증 정보는 client와 공유되지 않는다.
authorization code는 client를 인증하는 기능과 같은 몇 가지 중요한 보안 이점을 제공한다. resource owner의 user-agent를 통해 전달하지 않고, resource owner를 포함한 다른 사람들에게 잠재적으로 노출하지 않고 access token을 client에게 직접 전송한다.
A user agent is any software that retrieves and presents Web content for end users or is implemented using Web technologies. User agents include Web browsers, media players, and plug-ins that help in retrieving, rendering and interacting with Web content.
연관관계의주인(Owner) - 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.
단방향 연관관계 -다대일(N:1)
객체 연관관계 - 회원 객체는 Member.team 필드(멤버변수)로 팀 객체와 연관관계를 맺는다. - 회원 객체와 팀 객체는 단방향 관계이다. member.getTeam()으로 회원에서 팀의 조회는 가능하지만 그 반대는 불가능하다.
테이블 연관관계 - 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다. - 회원 테이블과 팀 테이블은 양방향관계이다. 회원 테이블의 TEAM_ID 외래 키를 통해 두 테이블을 서로 조인할 수 있다.
정리 - 객체는 참조(주소)로 연관관계를 맺는다. (a.getB().getC()) - 테이블은 외래 키로 연관관계를 맺는다. (JOIN) - 참조를 통한 연관관계는 언제나 단방향이다. 양방향으로 만드려면 서로 다른 단방향 관계 2개를 만들어 양쪽에서 참조하는 방식으로 구현할 수 있다. - 외래 키를 사용하는 테이블의 연관관계는 양방향이다.
객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 '객체 그래프 탐색'이라 한다.
테이블 연관관계
테이블 DDL
CREATE TABLE MEMBER(
MEMBER_ID VARCHAR(255) NOT NULL,
TEAM_ID VARCHAR(255),
USERNAME VARCHAR(255),
PRIMARY KEY (MEMBER_ID)
)
CREATE TABLE TEAM (
TEAM_ID VARCHAR(255) NOT NULL,
NAME VARCHAR(255),
PRIMARY KEY (TEAM_ID)
)
ALTER TABLE MEMBER ADD CONSTRAINT FK_MEMBER_TEAM
FOREIGN KEY (TEAM_ID)
REFERENCES TEAM
SQL로 조인하여 연관관계 탐색하기
- 회원1 과 회원2 소속시키기
INSERT INTO TEAM(TEAM_ID, NAME) VALUES('team1', '팀1');
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES('member1', 'team1', '회원1');
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES('member2', 'team1', '회원2');
- 회원1이 소속된 팀 조회하기
SELECT T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.NAME_ID
WHERE M.MEMBER_ID = 'member1'
JPA로 매핑해보기
// 매핑한 회원 엔티티
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
//연관관계 매핑
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
//연관관계 설정
public void setTeam(Team team) {
this.team = team;
}
//Getter, Setter ...
}
// 매핑한 팀 엔티티
@Entity
public class Team {
@Id
@Column (name = "TEAM_ID")
private String id;
private String name;
//Getter, Setter ...
}
객체 연관관계 : 회원 객체의 Member.team 필드 사용. 테이블 연관관계 : 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼을 사용
@ManyToOne : 이름 그대로 다대일(N:1) 관계라는 매핑 정보다. 연관관계 매핑 시에 이렇게 다중성을 나타내는 어노테이션을 필수로 사용한다. @JoinColumn(name="TEAM_ID") : 조인 컬럼은 외래 키를 매핑할 때 사용한다. name 속성에는 매핑할 외래 키 이름을 지정한다. 이 어노테이션은 생략 가능하다.
다대일과 비슷한 일대일 관계도 있다. 단방향 관계를 매핑할 때 둘 중 어떤 것을 사용해야 할지는 반대편 관계에 달려 있다. 반대편이 일대다 관계면 다대일을 사용하고 반대편이 일대일 관계면 일대일을 사용하면 된다. 참고로 일대일 관계는 다음 장에서 설명.
연관관계 사용
저장
회원과 팀을 저장하는 코드
public void testSave(){
//팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
//회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); //연관관계 설정 member1 -> team1 == 회원 -> 팀 참조
em.persist(member1); //저장
//회원2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); //연관관계 설정 member2 -> team1 == 회원 -> 팀 참조
em.persist(member2); //저장
}
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다. 영속성 컨텍스트에 등록되어야 CRUD 작업이 진핼할 수 있다.
조회
연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지다. - 객체 그래프 탐색(객체 연관관계를 사용한 조회) - 객체지향 쿼리 사용(JPQL)
/*
객체 그래프 탐색
*/
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
System.out.println("팀 이름 = " + team.getName());
// 출력 결과 : 팀 이름 = 팀1
/*
객체지향 쿼리 사용 - JPQL 조인 검색
*/
private static void queryLogicJoin(EntityManager em) {
String jpql = "select m from Member m join m.team t where ” +” t. name=: teamName ";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setParameter ("teamName", "팀 1");
.getResultList();
for (Member member : resultList) {
System. out. printin (" [query] member. username=,' +member.getUsername());
}
// 결과: [query] member.username = 회원1
// 결과: [query] member.username = 회원2
JPQL : SQL을 추상화한 객체 지향 쿼리 언어, 엔티티 객체를 대상으로 쿼리한다. SQL은 데이터베이스 테이블을 대상으로 쿼리 한다. : 부분은 바인딩 부분 (변수 넣을 수 있음)
수정
/*
연관관계를 수정하는 코드
*/
private static void updateRelation(EntityManager em) {
//새로운 팀 2
Team team2 = new Team(”team2”, ”팀2”);
em.persist(team2);
//회원 1 에 새로운 팀2 설정
Member member = em.find(Member.class, "memberl");
member.setTeam(team2);
}
/*
실행되는 수정 SQL
UPDATE MEMBER
SET
TEAM_ID='team2', ...
WHERE
ID='member1'
/*
수정 작업에서는 em.update() 같은 메소드가 없다. 단순히 불러온 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다. 그리고 변경사항을 데이터베이스에 자동으로 반영한다. 연관관계를 수정할 때도, 참조하는 대상만 변경하면 나머지는 JPA가 자동으로 처리한다.
연관관계 제거
/*
연관관계를 삭제하는 코드
*/
private static void deleteRelation(EntityManager em) {
Member member1 = em.find(Member.class, "member1");
member1.setTeam(null); //연관관계 제거
}
/*
실행되는 연관관계 제거 SQL
UPDATE MEMBER
SET
TEAM_ID=null, ...
WHERE
ID='member1'
*/
remove() 를 이용해 영속성 컨텍스트에서 제거하는 것이 중요
양방향 연관관계
데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있기에 데이터베이스에 추가할 내용은 없다. Team.members를 List 컬렉션으로 추가했다.
@OneToMany만 있으면 되지 mappedBy는 왜 필요할까? 객체에느 양방향 연관관계라는 것이 없다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다.
테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다. 엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 된다. 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따라서 둘 사이에 차이가 발생한다. 이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인(OWNER)이라 한다. 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다. 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.
양방향 매핑의 규칙: 연관관계의 주인
양방향 연관관계 매핑 시 지켜야 할 규칙이 있는데 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다. 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다. 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.
/*
연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
*/
class Team {
@OneToMany(mappedBy="team") //MappedBy 속성의 값은
//연관관계의 주인인 Member.team
private List<Member> members = new ArrayList<Member>();
...
}
정리: 연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다. 주인이 반대편(inverse, non-owning side)은 읽기만 가능하고 외래 키를 변경하지는 못한다.
참고> 데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 @ManyToOne에는 mappedBy 속성이 없다.
양방향 연관관계 저장 - 이 코드는 단방향 연관관계에서 살펴본 예제 5.6의 회원과 팀을 저장하는 코드와 완전히 같다.
/*
양방향 연관관계 저장
*/
public void teamSave(){
//팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
//회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); //연관관계 설정 member1 -> team1
em.persist(member1);
//회원2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); //연관관계 설정 member2 -> team1
em.persist(member2);
}
양방향 연관관계의 주의점
양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다. 데이터베이스에 외래 키값이 정상적으로 저장되지 않으며 이것부터 의심해보자.
public void testSaveNonOwner() {
//회원1 저장
/*
양방향 연관관계 주의점
*/
Member member 1 = new Member ("member 1", ''회원1”);
em.persist(member1);
//회원2 저장
Member member2 = new Member (”member2", "회원2”);
em.persist(member2);
Team teaml = new Team("teaml", ”팀 1”);
//주인이 아닌 곳만 연관관계 설정
teaml.getMembers().add(member1);
teaml.getMembers().add(member2);
em.persist(teaml);
}
MEMBER_ID
USERNAME
TEAM_ID
member1
회원1
null
member2
회원2
null
외래 키 TEAM_ID에 team1이 아닌 null 값이 입력되어 있는데, 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문이다. 연관관계의 주인만이 외래 키의 값을 변경할 수 있다.
순수한 객체까지 고려한 양방향 연관관계
좋은 방법은 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.
/*
JPA로 코드 완성
*/
public void testORM_양방향 () {
//팀1 저장
Team teaml : new Team ("teaml”, "팀 1”) ;
em.persist(team1);
Member member 1 = new Member ("member 1", "회원1”) ;
//양방향 연관관계 설정
member1.setTeam(teaml) ; //연관관계 설정 member 1 -> teaml
team1.getMembers().add(member1); //연관관계 설정 teaml -> member 1
em.persist(member1);
Member member2 = new Member ("meinber2", ”회원2”) ;
//양방향 연관관계 설정
member2.setTeam (teaml); //연관관계 설정 member2 -> teaml
team1.getMembers().add(member2); "연관관계 설정 teaml -> member2
em.persist(member2);
}
연관관계 편의 메소드
양방향 연관관계는 결국 양쪽 다 신경 써야 한다. 이전에는 member.setTeam(team)과 team.getMembers.add(member)를 각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있다. 양방향 관계에서 두 코드는 하나인 것처럼 사용하는 것이 안전하다.
/*
양방향 리팩토링 전체코드
*/
public void testORM_양방향_리팩토링() {
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); //양방향 설정
em.persist(member1);
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); //양방향 설정
em.persist(member2);
}
연관관계 편의 메소드 작성 시 주의사항
setTeam() 메소드의 버그 : 연관관계를 변경할 떄는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.
/*
기존 관계 제거
*/
public void setTeam(Team team) {
//기존 팀과 관계를 제거
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
정리
연관관계가 하나인 단방향 매핑은 언제나 연관관계의 주인이라는 점이다. 양방향은 여기에 주인이 아닌 연관관계를 하나 추가했을 뿐이다. 결국 단방향과 비교해서 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다.
연관관계의 주인을 정하는 기준
단방향은 항상 외래 키가 있는 곳을 기준으로 매핑하면 된다. 하지만 양방향은 연관관계의 주인(Owner)이라는 이름으로 인해 오해가 있을 수 있다. 비즈니스 로직상 더 중요하다고 연관 관계의 주인으로 선택하면 안 된다. 비즈니스 중요도를 배제하고 단순히 외래 키 관리자 정도의 의미만 부여해야 한다. 따라서 연관 관계의 주인은 외래 키의 위치와 관련해서 정해야지 비즈니스 중요도로 접근하면 안 된다.
양방향 매핑 시에는 무한 루프에 빠지지 않게 조심해야 한다. Member.toString()에서 getTeam()을 호출하고 Team.toString()에서 getMember()를 호출하면 무한 루프에 빠질 수 있다. 외래 키를 이용해서 연관된 객체를 가져오게 되서 팀이 멤버를 호출하고 멤버가 팀을 호출하고 무한 반복... @JsonIgnore @toStringExclude가 있으니 그걸 사용하면 해결 가능 (일 쪽에서 선언하면 됨)
일대다를 연관관계의 주인으로 선택하는 것이 불가능한 것만은 아니다. 팀 엔티티의 Team.members를 연관관계의 주인으로 선택할 수는 있다. 하지만 성능과 관리 측면에서 권장하지 않는다. 될 수 있으면 외래 키가 있는 곳을 연관관계의 주인으로 선택하자(다 쪽을 선택해라!)
이 글은 김영한 저, 자바 ORM 표준 JPA 프로그래밍 책을 정리한 글입니다. 모든 출처는 해당 책에 있습니다.
엔티티 매니저 팩토리와 엔티티 매니저 // 공장 만들기, 비용이 아주 많이 든다. EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook"); 위의 코드를 실행하고 나면 META-INF/persistence.xml에 있는 정보를 바탕으로 EntityManagerFactory를 생성한다. // 공장에서 엔티티 매니저 생성, 비용이 거의 안 든다. EntityManager em = emf.createEntityManager(); 이제 필요할 때마다 엔티티 매니저 팩토리에서 엔티티 매니저를 생성하면 된다. 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드 간에 공유해도 되지만, 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유하면 안 된다.
일반적인 웹 애플리케이션
엔티티 매니저는 데이터베이스 연결이 꼭 필요한 시점까지 커넥션을 얻지 않는다. JPA 구현체들은 공장을 만드는 단계에서 데이터베이스 커넥션 풀도 만드는 데 이것은 J2SE 환경에서 사용한다.
영속성 컨텍스트(Persistence context) 엔티티를 영구 저장하는 환경, persist() 메소드는 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장하는 과정이다. 이 컨텍스트에는 여러 매니저가 접근할 수도 있지만 하나의 매니저에 하나의 컨텍스트가 만들어 지는 것이 일반적이다.
엔티티의 생명주기 엔티티에는 4가지 상태가 있다. 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태, 객체를 생성한 뒤에 아직 저장하지 않은 객체. 영속(managed) : 영속성 컨텍스트에 저장된 상태. 즉, 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라 말함. 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태. 즉, 관리하던 객체를 관리하지 않게 된 상태. 삭제(removed) : 삭제된 상태. 즉, 영속성 컨텍스트와 데이터베이스에서 삭제된 상태.
영속성 컨텍스트의 특징
영속성 컨텍스트와 식별자 값 영속 상태는 식별자 값이 반드시 있어야 한다. 영속성 컨텍스트는 엔티티를 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분한다. 식별자 값이 없으면 예외가 발생한다.
영속성 컨텍스트와 데이터베이스 저장 JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영하는데 이것을 플러시(flush)라고 한다.
영속성 컨텍스트가 엔티티를 관리하면 다음과 같은 장점이 있다. - 1차 캐시 (영속성 컨텍스트가 내부에 가지고 있는 캐시, 영속 상태의 엔티티는 모두 이곳에 저장된다.) - 동일성 보장 - 트랜잭션을 지원하는 쓰기 지연 - 변경 감지 - 지연 로딩
엔티티 조회
영속성 컨텍스트 1차 캐시
1차 캐시의 키는 식별자 값이다. 그리고 식별자 값은 데이터베이스 기본 키와 매핑되어 있다. 즉, 영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 데이터베이스 기본 키 값이다.
만약 em.find() 를 호출했는데 엔티티가 1차 캐시에 없으면 엔티티 매니저는 데이터베이스를 조회해서 엔티티를 생성한다. 그리고 1차 캐시에 저장한 후에 영속 상태의 엔티티를 반환한다.
find () 메서드로 식별자가 같은 엔티티 인스턴스를 반복해서 호출해도 영속성 컨텍스트는 1차 캐시에 있는 같은 엔티티 인스턴스를 반환한다. 영속성 컨텍스트는 성능상 이점과 엔티티의 동일성을 보장한다. 동일성(identity): 실제 인스턴스가 같다. 따라서 참조 값을 비교하는 == 비교의 값이 같다. 동등성(equality): 실제 인스턴스는 다를 수 있지만 인스턴스가 가지고 있는 값이 같다. 자바에서 동등성 비교는 equals()메서드를 구현해야 한다.