JPQL은 객체지향 쿼리언어다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
JPQL은 결국 SQL로 변환된다.
2.1 기본 문법과 쿼리 API
JPQL은 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다.
INSERT는 EntityManger.persist() 메소드를 사용하면 되므로 없다.
SELECT
SELECT m FROM Member AS m where m.username = 'Hello'
대소문자 구분 - 예를 들어 Member, username은 대소문자를 구분한다. 반면 SELECT, FROM, AS 같은 키워드는 대소문자를 구분하지 않는다.
엔티티 이름 - Member는 클래스명이 아니라 엔티티 명이다.
별칭은 필수 - 별칭을 필수로 사용해야 한다. 별칭 없이 작성하면 잘못된 문법이라는 오류가 발생한다.
예) SELECT username FROM Member m
AS는 생략 가능하다.
하이버네이트 HQL(Hibernate Query Language)는 SELECT username FROM Member m 의 username처럼 별칭 없이 사용할 수 있다.
TypeQuery, Query
반환할 타입을 명확하게 지정할 수 있으면 TypeQuery 객체를 사용하고 지정할 수 없으면 Query를 사용하면 된다.
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> resultList = query.getResultList();
for(Member memeber : resultList) {
System.out.println("member = " + member);
}
Query query = em.createQuery("SELECT m.username, m.age from Member m");
List resultList = query.getResultList();
for(Object o : resultList) {
Object[] result = (Object[]) o;
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
}
위의 예제처럼 Query 객체는 SELECT 절의 조회 대상이 예제처럼 둘 이상이면 Object[]를 반환하고 SELECT 절이 조회 대상이 하나면 Object를 반환한다.
결과 조회
query.getResultList() : 결과를 예제로 반환한다. 결과가 없으면 빈 컬렉션을 반환한다.
query.getSingleResult() : 결과가 정확히 하나일 때 사용한다.
결과가 없으면 javax.persistence.NoResultEception 예외가 발생
결과가 1개보다 많으면 javax.persistence.NonUniqueResultException 예외가 발생
2.2 파라미터 바인딩
이름 기준 파라미터 : 이름 기준 파라미터는 파라미터를 이름으로 구분하는 방법이다. 파라미터 앞에 :를 사용한다.
String usernameParam = "User1";
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class);
query.setParameter("useranme", usernameParam); // :username이라는 이름 기준 파라미터를 정의하고 username이라는 이름으로 파라미터를 바인딩한다.
List<Member> resultList = query.getResultList();
또는
List<Member> members = em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class)
.setParameter("username", username)
.getResultList(); // 메소드 체인 방식으로 설계되어 있어 연속해서 작성할 수 있다.
위치 기준 파라미터 : 위치 기준 파라미터는 ? 다음에 위치 값을 주면 된다. 위치 값은 1부터 시작한다.
List<Member> members = em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class)
.setParameter(1, usernameParam)
.getResultList();
2.3 프로젝션
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 한다.
프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다.
스칼라 타입은 숫자, 문자 등 기본 데이터 타입을 뜻한다.
엔티티 프로젝션
SELECT m FROM Member m -- 회원
SELECT m.team FROM Member m -- 팀
처음에는 회원을 조회했고 두번째는 회원과 연관된 팀을 조회 했다.
둘 다 엔티티를 프로젝션 대상으로 사용했다.
원하는 객체를 바로 조회한 것인데 컬럼을 하나하나 나열해서 조회해야 하는 SQL과 차이가 있다.
SELECT order.city, order.street, order.zipcode FROM Orders order
임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.
스칼라 타입 프로젝션
숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다.
List<String> username = em.createQuery("SELECT username FROM Member m", String.class)
.getResultList();
Double orderAmountAvg = em.createQuery("SELECT AVG(o.orderAmount) FROM Order o", Double.class)
.getSingleResult();
여러 값 조회
프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 대신에 Query를 사용해야 한다.
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList();
Iterator iterator = resultList.iterator();
while(iterator.hasNext()) {
Object[] row = (Object[]) iterator.next();
String username = (String) row[0];
Integer age = (Integer) row[1];
}
// 제네릭 Object[]를 사용하면
List<Object[]> resultList = query.createQuery("SELECT m.username, m.age FROM Member m")
.getResultList();
for(Object[] row : resultList) {
String username = (String) row[0];
Integer age = (Integer) row[1];
}
// 스칼라 타입 뿐만 아니라 엔티티 타입도 여러값을 함께 조회할 수 있다.
List<Object[]> resultList = em.createQuery("SELECT o.member, o.product, o.orderAmount FROM Order o")
.getResultList();
for(Object[] row : resultList) {
Member member = (Member) row[0]; // 엔티티
Product product (Product) row[1]; // 엔티티
int orderAmount = (Integer) row[2]; // 스칼라
}
NEW 명령어
username, age 두 필드를 프로젝션해서 타입을 지정할 수 없으므로 TypeQuery를 사용할 수 없다. 따라서 Object[]를 반환받았다.
실제 개발시에는 UserDTO처럼 의미 있는 객체로 받아서 사용할 것이다.
List<Object[]> resultList = em.createQuery("SELECT m.username, m.age FROM Member m")
.getResultList();
// 객체 변환 작업
List<UserDTO> userDTOs = new ArrayList<UserDTO>();
for(Object[] row : resultList) {
UserDTO userDTO = new UserDTO((String) row[0], (Integer) row[2]);
userDTOs.add(userDTO);
}
return userDTOs;
public class UserDTO {
private String username;
private int age;
public UserDTO(String username, int age) {
this.username = username;
this.age = age;
}
// ...
}
// 이러한 객체 변환 작업 대신 NEW 명령어를 사용하면
TypedQuery<UserDTO> query = em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();
NEW 명령어를 사용할 때는 패키지 명을 포함한 전체 클래스 명을 입력, 순서와 타입이 일치하는 생성자가 필요하다.(패키지명이 바뀐다면 오류인데..)
2.4 페이징 API
JPA는 페이징을 두 API로 추상화 했다.
setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
setMaxResults(int maxResult) : 조회할 데이터 수
TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);
query.setFirstResult(10);
query.setMaxResult(20);
query.getResultList();
FirstResult의 시작은 0이므로 11번째부터 시작해 20건의 데이터를 조회한다.(11 ~ 30번 데이터)
데이터베이스마다 다른 방언을 알아서 잘 처리해주지만 SQL를 좀 더 최적화하고 싶다면 네이티브 SQL를 직접 사용해야 한다.
값이 없는데 SUM, AVG, MAX, MIN 함수를 사용하면 NULL이 된다. 단 COUNT는 0이 된다.
DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다.
예) select COUNT(DISTINCT m.age) from Member m
DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.
GROUP BY, HAVING
GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어준다.
HAVING은 GROUP BY와 함께 사용하는데 GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링 한다.
select t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
from Member m
LEFT JOIN m.team t
GROUP BY t.name
-- 다음 데이터 중 평균나이가 10살 이상인 그룹을 조회한다.
select t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
from Member m
LEFT JOIN m.team t
GROUP BY t.name
HAVING AVG(m.age) >= 10
-- 문법은 다음과 같다.
groupby_절 ::= GROUP BY {단일값 경로 | 별칭}+
having_절 ::= HAVING 조건식
통계 쿼리는 보통 전체 데이터를 기준으로 처리하므로 실시간으로 사용하기엔 부담이 많다.
결과가 아주 많다면 통계 결과만 저장하는 테이블을 별도로 만들어 두고 사용자가 적은 새벽에 통계 쿼리를 실행해서 그 결과를 보관하는 것이 좋다.
정렬(ORDER BY)
나이를 기준으로 내림차순으로 정렬하고 나이가 같으면 일므을 기준으로 오름차순으로 정렬
select m from Member m order by m.age DESC, m.username ASC
-- 문법은 다음과 같다.
orderby_절 ::= ORDER BY {상태필드 경로 | 결과 변수 [ASC | DESC]}+
select t.name, COUNT(m.age) as cnt
from Member m LEFT JOIN m.team t
GROUP BY t.name
ORDER BY cnt
2.6 JPQL 조인
내부 조인
내부 조인은 INNER JOIN을 사용하고 INNER는 생략할 수 있다.
String teamName = "팀A";
String query = "SELECT m FROM Member m INNER JOIN m.team t WHERE t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class)
.setParameter("teamName", teamName)
.getResultList();
SELECT m
FROM Member m INNER JOIN m.team t
where t.name = :teamName
-- 생성된 내부 조인 SQL
SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM
MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID
WHERE
T.NAME=?
JPQL 내부 조인 구문을 보면 SQL 조인과 약간 다른 것을 확인할 수 있다.
JPQL 조인의 가장 큰 특징은 연관 필드르 사용한다는 것이다. 여기서 m.team이 연관 필드인데 연관 필드는 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 말한다.
FROM Member m : 회원을 선택하고 m이라는 별칭을 주었다.
Member m JOIN m.team t : 회원이 가지고 있는 연관 필드로 팀과 조인한다. 조인한 팀에는 t라는 별칭을 주었다.
JPQL 조인을 SQL 조인처럼 사용하면 문법 오류가 발생한다.
=========== 추가 필요====================
외부 조인
SELECT m
FROM Member m LEFT (OUTER) JOIN m.team t
외부 조인은 기능상 SQL의 외부 조인과 같다.
OUTER는 생략 가능해서 봍통 LEFT JOIN으로 사용한다.
SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID
M.NAME AS NAME
FROM
MEMBER M LEFT OUTER JOIN TEAMM T ON M.TEAM_ID = T.ID
WHERE
T.NAME=?
컬렉션 조인
일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 한다.
[회원 -> 팀]으로의 조인은 다대일 조인이면서 단일 값 연관 필드(m.team)를 사용한다.
[팀 -> 회원]은 반대로 일대다 조인이면서 컬렉션 값 연고고나 필드(m.members)를 사용한다.
SELECT t, m FROM Team t LEFT JOIN t.members m
t LEFT JOIN t.members는 팀과 팀이 보유한 회원목록을 컬렉션 값 연관 필드로 외부 조인했다.
세타 조인
세타 조인은 내부 조인만 지원한며 세타 조인을 사용하면 전혀 관계없는 엔티티도 조인할 수 있다.
-- JPQL
select count(m) from Member m, Team t
where m.username = t.name
-- SQL
SELECT COUNT(M.ID)
FROM
MEMBER M CROSS JOIN TEAM T
WHERE
M.USERNAME=T.NAME
JOIN ON 절(JPA 2.1)
ON 절을 사용하면 조인 대상을 필터링하고 조인할 수 있다.
내부 조인의 ON 절은 WHERE 절을 사용할 때와 결과가 같으므로 보통 ON 절은 외부 조인에서만 사용한다.
모든 회원을 조회하면서 회원과 연관된 팀도 조회하자. 이때 팀은 이름이 A인 팀만 조회하자.
-- JPQL
select m, t from Member m
left join m.team t on t.name = 'A'
-- SQL
SELECT M.*, t.* FROM Member m
LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A'
2.7 페치 조인
페치 조인은 SQL에서 이야기하는 조인의 종류가 아닌 JPQL에서 성능 최적화를 위해 제공하는 기능이다.
연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능인데 join fetch 명령어로 사용할 수 있다.
문법
페치 조인 ::= [LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
엔티티 페치 조인
회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회하는 JPQL
select m
from Member m join fetch m.team
-- 실행된 sql
SELECT
M.*, T.*
FROM MEMBER T
INNER JOIN TEAM T ON M.TEAM_ID = T.ID
일반적인 JPQL 조인과는 다르게 m.team 다음에 별칭이 없다. 페치 조인은 별칭을 사용할 수 없다.(하이버네이트는 별칭 허용)
엔티티 페치 조인 JPQL에서 select m으로 회원 엔티티만 선택했는데 실행된 SQL을 보면 SELECT M., T.로 회원과 연관된 팀도 함게 조회된 것을 확인할 수 있다.
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
.getResultList();
for(Member member : members) {
// 페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 발생 안함
System.out.println("username = " + member.getUsername() + ", " + "teamname = " + member.getTeam().name());
}
회원과 팀을 지연 로딩으로 설정했다고 가정해보면 회원을 조회할 때 페치 조인을 사용해서 팀도 함게 조회했으므로 팀 엔티티는 프록시가 아닌 실제 엔티티다.
프록시가 아닌 실제 엔티티이므로 회원 엔티티가 영속성 컨텍스트에서 분리되어 준 영속 상태가 되어도 연관된 팀을 조회할 수 있다.
컬렉션 페치 조인
select t
from Team t join fetch t.members
where t.name = '팀A'
-- 실행된 SQL
SELECT
T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
컬렉션을 페치 조인한 JPQL에서 select t로 팀만 선택해도 T., M.로 팀과 연관된 회원도 함께 조회한 것을 확인할 수 있다.
2.8 경로 표현식
경로 표현식이란 쉽게 말해 .(점)을 찍어 객체 그래프를 탐색하는 것이다.
경로 표현식의 용어 정리
상태 필드 : 단순히 값을 저장하기 위한 필드
연관 필드 : 연관관계를 위한 필드, 임베디드 타입 포험
단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티
컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션
경로 표현식과 특징
상태 필드 경로 : 경로 탐색의 끝, 더이상 탐색할 수 없다.
단일 값 연관 경로 : 묵시적으로 내부 조인, 단일 값 연관 경로는 계속 탐색 가능
컬렉션 값 연관 경로 : 묵시적으로 내부 조인, 더는 탐색할 수 없다. 단 FROM절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색 가능
==== 추가필요 ====
2.9 서브쿼리
서브쿼리는 WHERE, HAVING 절에서만 사용가능하고 SELECT, FROM 절에서는 사용 할 수 없다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {...}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
...
private String author;
}
// ... Album, Movie 생략
// 다음과 같이 조회하면 Item의 자식도 함께 조회한다.
List resultList = em.createQuery("select i from Item i").getResultList();
-- 단일 테이블 전략을 사용할 때 실행되는 SQL (SINGLE_TABLE)
SELECT * FROM ITEM
-- 조인 전략을 사용할 떄 실행되는 SQL (JOINED)
SELECT
i.ITEM_ID, i.DTYPE, i.name, i.price, i.stockQuantity,
b.author, b,.isbn,
a.artist, a.etc,
m.actor, m.director
FROM
Item i
left outer join
Book b on i.ITEM_ID=b.ITEM_ID
left outer join
Album a on i.ITEM_ID=b,ITEM_ID
left outer join
Movie m on i.ITEM_ID=m.ITEM_ID
TYPE
TYPE은 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 떄 주로 사용
-- Item 중 Book, Movice를 조회
-- JPQL
select i from Item i
where type(i) IN (BOOK, MOVIE)
-- SQL
SELECT i FROM Item i
WHERE i.DTYPE in ('B', 'M')
TREAT(JPA 2.1)
자바의 타입 캐스팅과 비슷하다.
상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다.
JPA는 FROM, WHERE 사용할수 없음
하이버네이트는 SELECT 절에서도 TREAT 사용 가능
-- JPQL
select i from Item i where treat(i as Book).author = 'kim'
-- SQL
select i.* from Item i
where
i.DTYPE='B'
and i.author='kim'
2.12 사용자 정의 함수 호출
2.13 기타 정리
2.14 엔티티 직접 사용
기본 키 값
객체 인스턴스는 참조 값으로 식별하고 테이블 로우는 기본 키 값으로 식별한다.
JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용한다.
select count(m.id) from Member m
select count(m) from Member m
-- 실제 실행된 SQL
select count(m,id) as cnt
from Member m
-- 엔티티를 직접 사용하면 JPQL이 SQL로 변환될 때 해당 엔티티의 기본 키를 사용한다. 따라서 실제 실행된 SQL은 같다.
// 엔티티를 파라미터로 직접 받는 코드
String qlString = "select m from Member m where m = :member";
List resultList = em.createQuery(qlString)
.setParameter("member", member)
.getResultList();
-- 실행된 SQL
select m.*
from Member m
where m.id=?
외래 키 값
Team team = em.find(Team.class, 1L);
String qlString = "select m from Member m where m.team = :team";
List resultList = em.createQuery(qlString)
.setParameter("team", team)
.getResultList();
// 기본 키 값이 1L인 팀 엔티티를 파라미터로 사용하고 있고 m.team은 현재 team.id라는 외래 키와 매핑되어 있다.
select m.*
from Member m
where m.team_id=?(팀 파리미터의 ID 값)
엔티티 대신 식별자 값을 직접 사용할 수 있다.
m.team.id를 보면 Member와 Team 간에 묵시적 조인이 일어날 것 같지만 MEMBER 테이블이 team_id 외래 키를 가지고 있으므로 묵시적 조인은 일어나지 않는다.
m.team.name을 호출하면 묵시적 조인이 일어난다. 따라서 m.team를 사용하든 m.team.id를 사용하든 생성되는 SQL은 같다.
2.15 Named 쿼리 : 정적 쿼리'
동적 쿼리 : em.createQuery("select...")처럼 JPQL을 문자로 완성해서 직접 넘기는 것
정적 쿼리 : 미리 정의한 쿼리에 이름을 부여해 필요할 때 사용할 수 있는 것 한 번 정의하면 변경할 수 없는 정적 쿼리
Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해 오류를 빨리 확인할 수 있고, 결과를 재사용하므로 성능상 이점도 있다.
Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 데이터베이스의 조회 성능 최적화에도 도움이 된다.
@NamedQuery 어노테이션을 사용해서 자바 코드에 작성하거나 XML 문서에 작성
Named 쿼리를 어노테이션에 정의
@Entity
@NamedQuery(name = "Member.findByUsername", query="select m from Member m where m.username = :username")
public class Member {
// ....
}
@NamedQuery.name에 쿼리 이름을 부여하고 @NamedQuery.query에 사용할 쿼리를 입력하면 된다.
Named 쿼리 이름을 간단히 findByusername이라 하지 않고 MEmber.findByUsername처럼 앞에 인티티 이름을 주었는데 이것은 특별한 의미가 있는 것은 아니고 Named 쿼리는 영속성 유닛 단위로 관리되므로 충돌을 방지하기 위한 엔티티 이름을 앞에 준 것이다.
엔티티 이름이 앞에 있으면 관리하기가 쉽다.
하나의 엔티티에 2개 이상의 Named 쿼리를 정의하려면 @NamedQueries 어노테이션을 사용하면 된다.
@Entity
@NamedQueries({
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username")
@NamedQuery(
name = "Member.count",
query = "select count(m) from Member m")
})
public class Member {...}
10. 객체지향 쿼리언어
2 JPQL
2.1 기본 문법과 쿼리 API
SELECT
SELECT m FROM Member AS m where m.username = 'Hello'
TypeQuery, Query
결과 조회
2.2 파라미터 바인딩
2.3 프로젝션
엔티티 프로젝션
임베디드 타입 프로젝션
스칼라 타입 프로젝션
여러 값 조회
NEW 명령어
2.4 페이징 API
2.5 집합과 정렬
집합 함수
집합 함수 사용 시 참고사항
GROUP BY, HAVING
정렬(ORDER BY)
2.6 JPQL 조인
내부 조인
=========== 추가 필요====================
외부 조인
컬렉션 조인
세타 조인
JOIN ON 절(JPA 2.1)
2.7 페치 조인
엔티티 페치 조인
컬렉션 페치 조인
2.8 경로 표현식
경로 표현식의 용어 정리
@ManyToOne
,@OneToOne
, 대상이 엔티티@OneToMany
,@ManyToMany
, 대상이 컬렉션경로 표현식과 특징
==== 추가필요 ====
2.9 서브쿼리
서브 쿼리 함수
2.10 조건식
타입표현
작은 따옴표를 표현하고 싶으면 작은 따옴표 연속 두개 사용
'She''s'
D(Double 타입 지정)
F(Float 타입 지정)
10D
10F
TIME{t 'hh-mm-ss'}
DATETIME{ts 'yyyy-mm-dd hh:mm:ss.f}
{t '10-11-11'}
{ts '2012-03-24 10-11-11.123'}
m.createDate = {d '2012-03-24'}
2.11 다형성 쿼리
TYPE
TREAT(JPA 2.1)
2.12 사용자 정의 함수 호출
2.13 기타 정리
2.14 엔티티 직접 사용
기본 키 값
외래 키 값
2.15 Named 쿼리 : 정적 쿼리'
@NamedQuery
어노테이션을 사용해서 자바 코드에 작성하거나 XML 문서에 작성Named 쿼리를 어노테이션에 정의
@NamedQuery.name
에 쿼리 이름을 부여하고@NamedQuery.query
에 사용할 쿼리를 입력하면 된다.@NamedQueries
어노테이션을 사용하면 된다.