SlideShare uma empresa Scribd logo
1 de 43
Java Annotation과 MyBatis로
나만의 ORM Framework을 만들어보자
2011 JCO 11th Conference | Session ${track_#}-${session_#} | Javacommunity.Org
강동혁 (한솔헬스케어)
wolfkang@gmail.com
Revision: 20110612
하기 싫은 일
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection();
PreparedStatement stmt = c.prepareStatement
("select title, year_made from movies "
+ "where year_made >= ?"
+ " and year_made < ?");
for(int decadeStart = 1920; decadeStart < 2000; decadeStart += 10){
stmt.setInt(1, decadeStart);
stmt.setInt(2, decadeStart + 10);
ResultSet rs = stmt.executeQuery();
while(rs.next()){
System.out.println(rs.getString(1) + " (" + rs.getInt(2) + ")");
}
}
2
귀찮은 일
<insert id="insertDoctor" parameterType="domain.Doctor">
insert into Doctor (id,username,password,email,bio)
values (#{id},#{username},#{password},#{email},#{bio})
</insert>
<update id="updateDoctor" parameterType="domain.Doctor">
update Doctor set
username = #{username}, password = #{password},
email = #{email}, bio = #{bio}
where id = #{id}
</update>
<delete id="deleteDoctor” parameterType="int">
delete from Doctor where id = #{id}
</delete>
<select id="selectDoctor" resultType="domain.Doctor">
select * from Doctor
</select>
3
머리 아픈 일
<hibernate-mapping>
<class name="hello.Message“ table="MESSAGES">
<id name="id" column="MESSAGE_ID">
<generator class="increment"/>
</id>
<property name="text" column="MESSAGE_TEXT"/>
<many-to-one name="nextMessage" cascade="all"
column="NEXT_MESSAGE_ID"/>
</class>
</hibernate-mapping>
SELECT new list(mother, offspr, mate.name)
FROM DomesticCat AS mother
INNER JOIN mother.mate AS mate
LEFT OUTER JOIN mother.kittens AS offspr
4
하고 싶은 일
• SQL 작성 최소화
• Object oriented programming
• 로직에 집중
• 단순한 설정 - 쉽고, 직관적
• 학습의 최소화
5
ORM? Hibernate?
6
Hibernate + MyBatis?
7
고려해야 할 점
• 설정
– object, relation 매핑
• SQL 생성
– object 를 SQL 의 파라미터로 전달
– SQL 결과를 object 로 리턴
8
구현 전략
• object 와 relation 의 1:1 매핑 구현
– MyBatis 를 이용하여 기본적인 insert, update,
delete, select 문을 runtime 자동 생성
– 생성된 SQL문을 MyBatis SQL repository 에
저장
– Association 은 다루지 않음
– Join, subquery 구문은 MyBatis로 처리
• 설정은 Java Annotation 사용
• 일단 MySQL 먼저
9
Java Annotation
• 자바 5.0 에서 소개
• 자바 소스 코드에 추가되는 문법적인 메타데이터
• 클래스, 메소드, 변수, 파라미터, 패키지에 첨언하
여 컴파일러의 의해 .class 파일에 포함되어 컴파일
혹은 런타임 시 이용
• 번거로운 설정 작업들과 반복적인 코드를 줄여줌
• Built-in Annotations
– @Override, @Deprecated, @SupressWarnings
• Custom Annotations
– @Controller, @Service, @Entity, @Column
10
Custom Annotation
• @interface 선언
public @interface MyAnnotation {
String value();
}
• Annotation 추가
import MyAnnotation
@MyAnnotation(value=“my annotation”)
public void myMethod(int arg) {
// do something
}
11
Annotation API
• Class 와 Field
– getAnnotation(Class<A> annotationClass)
특정 타입에 대한 annotation 을 리턴
– getAnnotations()
모든 타입의 annotation 을 리턴
– isAnnotationPresent(Class annotationClass)
특정 타입에 대한 annotation 이 존재하면
true 리턴
12
MyBatis
• 구) iBatis
• SQL문과 객체를 매핑
• SQL문을 XML 파일에 저장
• JDBC 코드 제거
13
출처) www.mybatis.org
MyBatis 예제
• Mapped Statement
<mapper namespace="org.mybatis.example.HospitalMapper">
<select id="selectHospital" parameterType="int"
resultType="Hospital">
select * from Hospital where id = #{id}
</select>
</mapper>
• Java DAO code
public interface HospitalMapper {
public Hospital selectHospital(int id);
}
HospitalMapper mapper = session.getMapper(HospitalMapper.class);
Hospital hospital = mapper.selectHospital(101);
14
순서
1. Table 생성
2. Annotation 정의
3. Mapping class 생성
4. EntityManager 작성 – CRUD interface
5. SQL Generation 클래스 작성
– InsertSqlSource
– UpdateSqlSource
– DeleteSqlSource
– SelectOneSqlSource
– SelectListSqlSource
15
1. Table 생성
Create table hospital (
hospitalid INT NOT NULL
PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(30),
regdttm DATETIME
)
16
2. Annotation 정의
public @interface Table {
String value() default "";
}
public @interface Column {
String name() default "";
boolean primaryKey() default false;
boolean autoIncrement() default false;
}
17
3. Mapping Class
@Table(“hospital”)
Public class Hospital {
@Column(primaryKey=true,
autoIncrement=true)
private Integer hospitalid;
@Column private String name;
@Column(name=“regdttm”)
private Date regdttm;
}
18
4. EntityManager
void insert(Object parameter)
void update(Object parameter)
void delete(Object parameter)
Object load(Object parameter)
List list(Object parameter)
List list(Object parameter, String orderby)
List list(Object parameter, String orderby,
int rows)
19
EntityManager 사용예
EntityManager entityManager = new EntityManager();
Hospital hospital = new Hospital();
hospital.setHospitalid(12345);
hospital.setName(“JCO병원”);
hospital.setRegdttm(new Date());
entityManager.insert(hospital);
hospital = entityManager.load(hospital);
hospital.setName(“JCO병원”);
entityManager.update(hospital);
entityManager.delete(hospital);
20
5. SQL Generation
• INSERT
– 컬럼 중 auto_increment 로 선언된 컬럼은 insert 문
에서 제외
• UPDATE
– Primary Key 가 아닌 컬럼들만 update
– PK로 where 조건절 생성
• DELETE
– parameter 객체에서 null 값이 아닌 field 로 where 조
건절 생성
• SELECT
– parameter 객체에서 null 값이 아닌 field 로 where 조
건절 생성
21
5-1. Insert문 생성
• 컬럼 중 auto_increment 로 선언된 컬럼은
insert 문에서 제외
<생성되는 SQL>
INSERT INTO hospital (name, regdttm)
VALUES (#{name}, #{regdttm})
22
EntityManager.insert()
1: Class<?> clazz = object.getClass();
2: String statementName = PREFIX_INSERT +
3: clazz.getSimpleName();
4: if (!configuration.hasStatement(statementName)) {
5: addMappedStatement(
6: statementName,
7: new InsertSqlSource(sqlSourceParser,clazz),
8: SqlCommandType.INSERT,null);
9: }
10: getSqlSession().insert(statementName, object);
23
InsertSqlSource
1: List<String> columnNames =
2: AnnotationUtil.getNonAutoIncrementColumnNames(clazz);
3: String sql = String.format(
4: “INSERT INTO %1$s ( %2$s ) VALUES ( %3$s ) ",
5: AnnotationUtil.getTableName(clazz),
6: StringUtil.join(columnNames, ","),
7: StringUtil.join(columnNames, "#{%1$s}",","));
8: parse(sqlSourceParser, sql, clazz);
24
AnnotationUtil.getTableName
Table t = clazz.getAnnotation(Table.class);
return t.value();
25
AnnotationUtil.getNonAutoIncrem
entColumnNames
1: List<String> names = new LinkedList<String>();
2: for (Field field : clazz.getDeclaredFields()) {
3: if (field.isAnnotationPresent(Column.class)) {
4: Column c = field.getAnnotation(Column.class);
5: if (!c.autoIncrement())
6: names.add("".equals(c.name()) ? field.getName():c.name());
7: }
8: }
9: return names;
26
5-2. Update문 생성
• Primary Key 가 아닌 컬럼들만 update
• PK로 where 조건절 생성
<생성되는 SQL>
UPDATE hospital
SET name = #{name}, regdttm = #{regdttm}
WHERE hospitalid = #{hospitalid}
27
EntityManager.update()
1: Class<?> clazz = object.getClass();
2: String statementName = PREFIX_UPDATE +
3: clazz.getSimpleName();
4: if (!configuration.hasStatement(statementName)) {
5: addMappedStatement(
6: statementName,
7: new UpdateSqlSource(sqlSourceParser,clazz),
8: SqlCommandType.UPDATE,null);
9: }
10: getSqlSession().update(statementName, object);
28
UpdateSqlSource
String sql = String.format(
"UPDATE %1$s SET %2$s WHERE %3$s",
AnnotationUtil.getTableName(clazz),
StringUtil.join(
AnnotationUtil.getNonPrimaryKeyColumnNames(clazz),
"%1$s = #{%1$s}", ", "),
StringUtil.join(
AnnotationUtil.getPrimaryKeyColumnNames(clazz),
"%1$s = #{%1$s}"," AND "));
parse(sqlSourceParser, sql, clazz);
29
AnnotationUtil.getNonPrimaryKey
ColumnNames
1: List<String> names = new LinkedList<String>();
2: for (Field field : clazz.getDeclaredFields()) {
3: if (field.isAnnotationPresent(Column.class)) {
4: Column c = field.getAnnotation(Column.class);
5: if (!c.primaryKey())
6: names.add("".equals(c.name()) ? field.getName() :
7: c.name());
8: }
9: }
10: return names;
30
AnnotationUtil.getPrimaryKeyColu
mnNames
List<String> names = new LinkedList<String>();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Column.class)) {
Column c = field.getAnnotation(Column.class);
if (c.primaryKey())
names.add("".equals(c.name()) ? field.getName() : c.name());
}
}
return names;
31
5-3. Delete문 생성
• parameter 객체에서 null 값이 아닌 field 로
where 조건절 생성
예)
Hospital hospital = new Hospital();
hospital.setName(“JCO병원”);
entityManager.delete(hospital);
<생성되는 SQL>
DELETE FROM hospital
WHERE name = #{name}
32
EntityManager.delete()
1: Class<?> clazz = object.getClass();
2: String statementName = PREFIX_DELETE + clazz.getSimpleName();
3: if (!configuration.hasStatement(statementName)) {
4: addMappedStatement(
5: statementName,
6: new DeleteSqlSource(sqlSourceParser,clazz),
7: SqlCommandType.DELETE,null);
8: }
9: getSqlSession().delete(statementName, object);
33
DeleteSqlSource
1: public DeleteSqlSource(SqlSourceBuilder sqlSourceParser,
2: Class<?> clazz) {
3: super(sqlSourceParser);
4: staticSql = "DELETE FROM”
5: +AnnotationUtil.getTableName(clazz);
6: }
7: public BoundSql getBoundSql(Object parameterObject) {
8: String sql = staticSql + " WHERE " +
9: StringUtil.join(
10: AnnotationUtil.getNotNullColumnNames(parameterObject),
11: "%1$s = #{%1$s}"," AND ");
12: return getBoundSql(sql,parameterObject);
13: }
34
5-4. Select문 생성
• parameter 객체에서 null 값이 아닌 field 로 where
조건절 생성
예)
Hospital hospital = new Hospital();
hospital.setName(“JCO병원”);
hospital = (Hospital)entityManager.load(hospital);
<생성되는 SQL>
SELECT hospitalid, name, regdttm
FROM hospital
WHERE name = #{name}
35
EntityManager.load()
1: Class<?> clazz = object.getClass();
2: String statementName = PREFIX_LOAD + clazz.getSimpleName();
3: if (!configuration.hasStatement(statementName)) {
4: addMappedStatement(statementName,
5: new SelectOneSqlSource(sqlSourceParser,clazz),
6: SqlCommandType.SELECT,clazz);
7: }
8: Object result = getSqlSession().selectOne(statementName, object);
9: if (result != null)
10: BeanUtils.copyProperties(result, object);
11: return result;
36
SelectOneSqlSource
1: public SelectOneSqlSource(SqlSourceBuilder sqlSourceParser, Class<?> clazz) {
2: super(sqlSourceParser);
3: staticSql = String.format("SELECT %1$s FROM %2$s ",
4: StringUtil.join(AnnotationUtil.getColumnNames(clazz),", "),
5: AnnotationUtil.getTableName(clazz));
6: }
7:
8: public BoundSql getBoundSql(Object parameterObject) {
9: String sql = staticSql + " WHERE " +
10: StringUtil.join(
11: AnnotationUtil.getNotNullColumnNames(parameterObject),
12: "%1$s = #{%1$s}"," AND ") +
13: " LIMIT 1";
14: return getBoundSql(sql,parameterObject);
15: }
37
복습
1. Table 생성
2. Annotation 정의
3. Mapping class 생성
4. EntityManager 작성 – CRUD interface
5. SQL Generation 클래스 작성
– InsertSqlSource
– UpdateSqlSource
– DeleteSqlSource
– SelectOneSqlSource
– SelectListSqlSource
38
Effect
• 비타민MD 사이트에 일부 적용
• 만약 전체 적용한다면
69% SQL문 제거 가능
39
SQL 적용 전 적용 후
insert 112 7
update 80 2
delete 79 0
select 327 176
총 598 185
Future Work
• Annotation 추가
– Like 구문 지원
– DDL 상의 default 값, not null 등의 constraint
지원
• Transaction
• Caching
• Oracle, MSSQL, DB2 등 다른 DBMS 지원
40
Contact
http://code.google.com/p/mybatis-orm
wolfkang@gmail.com
41
Do It Yourself and Have Fun.
42
이 저작물은 크리에이티브 커먼스 코리아 저작자표시-비영리-
동일조건변경허락 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
This work is Licensed under Creative Commons Korea Attribution 2.0 License.

Mais conteúdo relacionado

Mais procurados

효율적인 SQL 작성방법 1주차
효율적인 SQL 작성방법 1주차효율적인 SQL 작성방법 1주차
효율적인 SQL 작성방법 1주차희동 강
 
EcmaScript6(2015) Overview
EcmaScript6(2015) OverviewEcmaScript6(2015) Overview
EcmaScript6(2015) Overviewyongwoo Jeon
 
java 8 람다식 소개와 의미 고찰
java 8 람다식 소개와 의미 고찰java 8 람다식 소개와 의미 고찰
java 8 람다식 소개와 의미 고찰Sungchul Park
 
빠르게 활용하는 파이썬3 스터디(ch1~4)
빠르게 활용하는 파이썬3 스터디(ch1~4)빠르게 활용하는 파이썬3 스터디(ch1~4)
빠르게 활용하는 파이썬3 스터디(ch1~4)SeongHyun Ahn
 
[Pgday.Seoul 2021] 1. 예제로 살펴보는 포스트그레스큐엘의 독특한 SQL
[Pgday.Seoul 2021] 1. 예제로 살펴보는 포스트그레스큐엘의 독특한 SQL[Pgday.Seoul 2021] 1. 예제로 살펴보는 포스트그레스큐엘의 독특한 SQL
[Pgday.Seoul 2021] 1. 예제로 살펴보는 포스트그레스큐엘의 독특한 SQLPgDay.Seoul
 
[Main Session] 미래의 Java 미리보기 - 앰버와 발할라 프로젝트를 중심으로
[Main Session] 미래의 Java 미리보기 - 앰버와 발할라 프로젝트를 중심으로[Main Session] 미래의 Java 미리보기 - 앰버와 발할라 프로젝트를 중심으로
[Main Session] 미래의 Java 미리보기 - 앰버와 발할라 프로젝트를 중심으로Oracle Korea
 
자바8 람다 나머지 공개
자바8 람다 나머지 공개자바8 람다 나머지 공개
자바8 람다 나머지 공개Sungchul Park
 
(국비지원/실업자교육/재직자교육/스프링교육/마이바티스교육추천)#13.스프링프레임워크 & 마이바티스 (Spring Framework, MyB...
(국비지원/실업자교육/재직자교육/스프링교육/마이바티스교육추천)#13.스프링프레임워크 & 마이바티스 (Spring Framework, MyB...(국비지원/실업자교육/재직자교육/스프링교육/마이바티스교육추천)#13.스프링프레임워크 & 마이바티스 (Spring Framework, MyB...
(국비지원/실업자교육/재직자교육/스프링교육/마이바티스교육추천)#13.스프링프레임워크 & 마이바티스 (Spring Framework, MyB...탑크리에듀(구로디지털단지역3번출구 2분거리)
 
파이썬+함수이해하기 20160229
파이썬+함수이해하기 20160229파이썬+함수이해하기 20160229
파이썬+함수이해하기 20160229Yong Joon Moon
 
Es2015 Simple Overview
Es2015 Simple OverviewEs2015 Simple Overview
Es2015 Simple OverviewKim Hunmin
 
UML distilled 1장 스터디 발표 자료
UML distilled 1장 스터디 발표 자료UML distilled 1장 스터디 발표 자료
UML distilled 1장 스터디 발표 자료beom kyun choi
 
자바8 람다식 소개
자바8 람다식 소개자바8 람다식 소개
자바8 람다식 소개beom kyun choi
 
120908 레거시코드활용전략 4장5장
120908 레거시코드활용전략 4장5장120908 레거시코드활용전략 4장5장
120908 레거시코드활용전략 4장5장tedypicker
 
제 5회 엑셈 수요 세미나 자료 연구컨텐츠팀
제 5회 엑셈 수요 세미나 자료 연구컨텐츠팀제 5회 엑셈 수요 세미나 자료 연구컨텐츠팀
제 5회 엑셈 수요 세미나 자료 연구컨텐츠팀EXEM
 
제 7회 엑셈 수요 세미나 자료 연구컨텐츠팀
제 7회 엑셈 수요 세미나 자료 연구컨텐츠팀제 7회 엑셈 수요 세미나 자료 연구컨텐츠팀
제 7회 엑셈 수요 세미나 자료 연구컨텐츠팀EXEM
 
모델링 연습 리뷰
모델링 연습 리뷰모델링 연습 리뷰
모델링 연습 리뷰beom kyun choi
 
R 스터디 세번째
R 스터디 세번째R 스터디 세번째
R 스터디 세번째Jaeseok Park
 
track2 04. MS는 Rx를 왜 만들었을까? feat. RxJS/ 네이버, 김훈민
track2 04. MS는 Rx를 왜 만들었을까? feat. RxJS/ 네이버, 김훈민 track2 04. MS는 Rx를 왜 만들었을까? feat. RxJS/ 네이버, 김훈민
track2 04. MS는 Rx를 왜 만들었을까? feat. RxJS/ 네이버, 김훈민 양 한빛
 
파이썬+객체지향+이해하기 20160131
파이썬+객체지향+이해하기 20160131파이썬+객체지향+이해하기 20160131
파이썬+객체지향+이해하기 20160131Yong Joon Moon
 

Mais procurados (20)

효율적인 SQL 작성방법 1주차
효율적인 SQL 작성방법 1주차효율적인 SQL 작성방법 1주차
효율적인 SQL 작성방법 1주차
 
EcmaScript6(2015) Overview
EcmaScript6(2015) OverviewEcmaScript6(2015) Overview
EcmaScript6(2015) Overview
 
java 8 람다식 소개와 의미 고찰
java 8 람다식 소개와 의미 고찰java 8 람다식 소개와 의미 고찰
java 8 람다식 소개와 의미 고찰
 
빠르게 활용하는 파이썬3 스터디(ch1~4)
빠르게 활용하는 파이썬3 스터디(ch1~4)빠르게 활용하는 파이썬3 스터디(ch1~4)
빠르게 활용하는 파이썬3 스터디(ch1~4)
 
[Pgday.Seoul 2021] 1. 예제로 살펴보는 포스트그레스큐엘의 독특한 SQL
[Pgday.Seoul 2021] 1. 예제로 살펴보는 포스트그레스큐엘의 독특한 SQL[Pgday.Seoul 2021] 1. 예제로 살펴보는 포스트그레스큐엘의 독특한 SQL
[Pgday.Seoul 2021] 1. 예제로 살펴보는 포스트그레스큐엘의 독특한 SQL
 
[Main Session] 미래의 Java 미리보기 - 앰버와 발할라 프로젝트를 중심으로
[Main Session] 미래의 Java 미리보기 - 앰버와 발할라 프로젝트를 중심으로[Main Session] 미래의 Java 미리보기 - 앰버와 발할라 프로젝트를 중심으로
[Main Session] 미래의 Java 미리보기 - 앰버와 발할라 프로젝트를 중심으로
 
자바8 람다 나머지 공개
자바8 람다 나머지 공개자바8 람다 나머지 공개
자바8 람다 나머지 공개
 
(국비지원/실업자교육/재직자교육/스프링교육/마이바티스교육추천)#13.스프링프레임워크 & 마이바티스 (Spring Framework, MyB...
(국비지원/실업자교육/재직자교육/스프링교육/마이바티스교육추천)#13.스프링프레임워크 & 마이바티스 (Spring Framework, MyB...(국비지원/실업자교육/재직자교육/스프링교육/마이바티스교육추천)#13.스프링프레임워크 & 마이바티스 (Spring Framework, MyB...
(국비지원/실업자교육/재직자교육/스프링교육/마이바티스교육추천)#13.스프링프레임워크 & 마이바티스 (Spring Framework, MyB...
 
파이썬+함수이해하기 20160229
파이썬+함수이해하기 20160229파이썬+함수이해하기 20160229
파이썬+함수이해하기 20160229
 
Es2015 Simple Overview
Es2015 Simple OverviewEs2015 Simple Overview
Es2015 Simple Overview
 
UML distilled 1장 스터디 발표 자료
UML distilled 1장 스터디 발표 자료UML distilled 1장 스터디 발표 자료
UML distilled 1장 스터디 발표 자료
 
자바8 람다식 소개
자바8 람다식 소개자바8 람다식 소개
자바8 람다식 소개
 
120908 레거시코드활용전략 4장5장
120908 레거시코드활용전략 4장5장120908 레거시코드활용전략 4장5장
120908 레거시코드활용전략 4장5장
 
제 5회 엑셈 수요 세미나 자료 연구컨텐츠팀
제 5회 엑셈 수요 세미나 자료 연구컨텐츠팀제 5회 엑셈 수요 세미나 자료 연구컨텐츠팀
제 5회 엑셈 수요 세미나 자료 연구컨텐츠팀
 
제 7회 엑셈 수요 세미나 자료 연구컨텐츠팀
제 7회 엑셈 수요 세미나 자료 연구컨텐츠팀제 7회 엑셈 수요 세미나 자료 연구컨텐츠팀
제 7회 엑셈 수요 세미나 자료 연구컨텐츠팀
 
모델링 연습 리뷰
모델링 연습 리뷰모델링 연습 리뷰
모델링 연습 리뷰
 
R 스터디 세번째
R 스터디 세번째R 스터디 세번째
R 스터디 세번째
 
track2 04. MS는 Rx를 왜 만들었을까? feat. RxJS/ 네이버, 김훈민
track2 04. MS는 Rx를 왜 만들었을까? feat. RxJS/ 네이버, 김훈민 track2 04. MS는 Rx를 왜 만들었을까? feat. RxJS/ 네이버, 김훈민
track2 04. MS는 Rx를 왜 만들었을까? feat. RxJS/ 네이버, 김훈민
 
JDK 변천사
JDK 변천사JDK 변천사
JDK 변천사
 
파이썬+객체지향+이해하기 20160131
파이썬+객체지향+이해하기 20160131파이썬+객체지향+이해하기 20160131
파이썬+객체지향+이해하기 20160131
 

Semelhante a Java Annotation과 MyBatis로 나만의 ORM Framework을 만들어보자

2014.07.26 KSUG와 지앤선이 함께하는 테크니컬 세미나 - 나의 첫번째 자바8 람다식 (정대원)
2014.07.26 KSUG와 지앤선이 함께하는 테크니컬 세미나 - 나의 첫번째 자바8 람다식 (정대원)2014.07.26 KSUG와 지앤선이 함께하는 테크니컬 세미나 - 나의 첫번째 자바8 람다식 (정대원)
2014.07.26 KSUG와 지앤선이 함께하는 테크니컬 세미나 - 나의 첫번째 자바8 람다식 (정대원)JiandSon
 
SpringCamp 2013 : About Jdk8
SpringCamp 2013 : About Jdk8SpringCamp 2013 : About Jdk8
SpringCamp 2013 : About Jdk8Sangmin Lee
 
나에 첫번째 자바8 람다식 지앤선
나에 첫번째 자바8 람다식   지앤선나에 첫번째 자바8 람다식   지앤선
나에 첫번째 자바8 람다식 지앤선daewon jeong
 
불어오는 변화의 바람, From c++98 to c++11, 14
불어오는 변화의 바람, From c++98 to c++11, 14 불어오는 변화의 바람, From c++98 to c++11, 14
불어오는 변화의 바람, From c++98 to c++11, 14 명신 김
 
[D2 오픈세미나]5.robolectric 안드로이드 테스팅
[D2 오픈세미나]5.robolectric 안드로이드 테스팅[D2 오픈세미나]5.robolectric 안드로이드 테스팅
[D2 오픈세미나]5.robolectric 안드로이드 테스팅NAVER D2
 
Java mentoring of samsung scsc 2
Java mentoring of samsung scsc   2Java mentoring of samsung scsc   2
Java mentoring of samsung scsc 2도현 김
 
스프링처럼 JDBC 리팩터링하기
스프링처럼 JDBC 리팩터링하기 스프링처럼 JDBC 리팩터링하기
스프링처럼 JDBC 리팩터링하기 Chanwook Park
 
Refactoring - Chapter 8.2
Refactoring - Chapter 8.2Refactoring - Chapter 8.2
Refactoring - Chapter 8.2Ji Ung Lee
 
파이썬 데이터베이스 연결 2탄
파이썬 데이터베이스 연결 2탄파이썬 데이터베이스 연결 2탄
파이썬 데이터베이스 연결 2탄SeongHyun Ahn
 
테스트 가능한 소프트웨어 설계와 TDD작성 패턴 (Testable design and TDD)
테스트 가능한 소프트웨어 설계와 TDD작성 패턴 (Testable design and TDD)테스트 가능한 소프트웨어 설계와 TDD작성 패턴 (Testable design and TDD)
테스트 가능한 소프트웨어 설계와 TDD작성 패턴 (Testable design and TDD)Suwon Chae
 
ECMAScript 6의 새로운 것들!
ECMAScript 6의 새로운 것들!ECMAScript 6의 새로운 것들!
ECMAScript 6의 새로운 것들!WooYoung Cho
 
파이썬 스터디 15장
파이썬 스터디 15장파이썬 스터디 15장
파이썬 스터디 15장SeongHyun Ahn
 
2시간만에 자바 데이터처리를 쉽게 배우고 싶어요.
2시간만에  자바 데이터처리를 쉽게 배우고 싶어요.2시간만에  자바 데이터처리를 쉽게 배우고 싶어요.
2시간만에 자바 데이터처리를 쉽게 배우고 싶어요.Kenu, GwangNam Heo
 
제1회 Tech Net Sql Server 2005 T Sql Enhancements
제1회 Tech Net Sql Server 2005 T Sql Enhancements제1회 Tech Net Sql Server 2005 T Sql Enhancements
제1회 Tech Net Sql Server 2005 T Sql Enhancementsbeamofhope
 
함수형 사고 - Functional thinking
함수형 사고 - Functional thinking함수형 사고 - Functional thinking
함수형 사고 - Functional thinking재문 심
 
#20.스프링프레임워크 & 마이바티스 (Spring Framework, MyBatis)_국비지원IT학원/실업자/재직자환급교육/자바/스프링/...
#20.스프링프레임워크 & 마이바티스 (Spring Framework, MyBatis)_국비지원IT학원/실업자/재직자환급교육/자바/스프링/...#20.스프링프레임워크 & 마이바티스 (Spring Framework, MyBatis)_국비지원IT학원/실업자/재직자환급교육/자바/스프링/...
#20.스프링프레임워크 & 마이바티스 (Spring Framework, MyBatis)_국비지원IT학원/실업자/재직자환급교육/자바/스프링/...탑크리에듀(구로디지털단지역3번출구 2분거리)
 

Semelhante a Java Annotation과 MyBatis로 나만의 ORM Framework을 만들어보자 (20)

2014.07.26 KSUG와 지앤선이 함께하는 테크니컬 세미나 - 나의 첫번째 자바8 람다식 (정대원)
2014.07.26 KSUG와 지앤선이 함께하는 테크니컬 세미나 - 나의 첫번째 자바8 람다식 (정대원)2014.07.26 KSUG와 지앤선이 함께하는 테크니컬 세미나 - 나의 첫번째 자바8 람다식 (정대원)
2014.07.26 KSUG와 지앤선이 함께하는 테크니컬 세미나 - 나의 첫번째 자바8 람다식 (정대원)
 
SpringCamp 2013 : About Jdk8
SpringCamp 2013 : About Jdk8SpringCamp 2013 : About Jdk8
SpringCamp 2013 : About Jdk8
 
나에 첫번째 자바8 람다식 지앤선
나에 첫번째 자바8 람다식   지앤선나에 첫번째 자바8 람다식   지앤선
나에 첫번째 자바8 람다식 지앤선
 
불어오는 변화의 바람, From c++98 to c++11, 14
불어오는 변화의 바람, From c++98 to c++11, 14 불어오는 변화의 바람, From c++98 to c++11, 14
불어오는 변화의 바람, From c++98 to c++11, 14
 
[D2 오픈세미나]5.robolectric 안드로이드 테스팅
[D2 오픈세미나]5.robolectric 안드로이드 테스팅[D2 오픈세미나]5.robolectric 안드로이드 테스팅
[D2 오픈세미나]5.robolectric 안드로이드 테스팅
 
Java mentoring of samsung scsc 2
Java mentoring of samsung scsc   2Java mentoring of samsung scsc   2
Java mentoring of samsung scsc 2
 
스프링처럼 JDBC 리팩터링하기
스프링처럼 JDBC 리팩터링하기 스프링처럼 JDBC 리팩터링하기
스프링처럼 JDBC 리팩터링하기
 
Refactoring - Chapter 8.2
Refactoring - Chapter 8.2Refactoring - Chapter 8.2
Refactoring - Chapter 8.2
 
파이썬 데이터베이스 연결 2탄
파이썬 데이터베이스 연결 2탄파이썬 데이터베이스 연결 2탄
파이썬 데이터베이스 연결 2탄
 
테스트 가능한 소프트웨어 설계와 TDD작성 패턴 (Testable design and TDD)
테스트 가능한 소프트웨어 설계와 TDD작성 패턴 (Testable design and TDD)테스트 가능한 소프트웨어 설계와 TDD작성 패턴 (Testable design and TDD)
테스트 가능한 소프트웨어 설계와 TDD작성 패턴 (Testable design and TDD)
 
ECMAScript 6의 새로운 것들!
ECMAScript 6의 새로운 것들!ECMAScript 6의 새로운 것들!
ECMAScript 6의 새로운 것들!
 
파이썬 스터디 15장
파이썬 스터디 15장파이썬 스터디 15장
파이썬 스터디 15장
 
2시간만에 자바 데이터처리를 쉽게 배우고 싶어요.
2시간만에  자바 데이터처리를 쉽게 배우고 싶어요.2시간만에  자바 데이터처리를 쉽게 배우고 싶어요.
2시간만에 자바 데이터처리를 쉽게 배우고 싶어요.
 
제1회 Tech Net Sql Server 2005 T Sql Enhancements
제1회 Tech Net Sql Server 2005 T Sql Enhancements제1회 Tech Net Sql Server 2005 T Sql Enhancements
제1회 Tech Net Sql Server 2005 T Sql Enhancements
 
miss_pattern_v2
miss_pattern_v2miss_pattern_v2
miss_pattern_v2
 
함수형 사고 - Functional thinking
함수형 사고 - Functional thinking함수형 사고 - Functional thinking
함수형 사고 - Functional thinking
 
#20.스프링프레임워크 & 마이바티스 (Spring Framework, MyBatis)_국비지원IT학원/실업자/재직자환급교육/자바/스프링/...
#20.스프링프레임워크 & 마이바티스 (Spring Framework, MyBatis)_국비지원IT학원/실업자/재직자환급교육/자바/스프링/...#20.스프링프레임워크 & 마이바티스 (Spring Framework, MyBatis)_국비지원IT학원/실업자/재직자환급교육/자바/스프링/...
#20.스프링프레임워크 & 마이바티스 (Spring Framework, MyBatis)_국비지원IT학원/실업자/재직자환급교육/자바/스프링/...
 
Java stream v0.1
Java stream v0.1Java stream v0.1
Java stream v0.1
 
Java stream v0.1
Java stream v0.1Java stream v0.1
Java stream v0.1
 
함수적 사고 2장
함수적 사고 2장함수적 사고 2장
함수적 사고 2장
 

Mais de Donghyeok Kang

Divi custom post type template
Divi custom post type templateDivi custom post type template
Divi custom post type templateDonghyeok Kang
 
My second word press plugin
My second word press pluginMy second word press plugin
My second word press pluginDonghyeok Kang
 
My first word press plugin
My first word press pluginMy first word press plugin
My first word press pluginDonghyeok Kang
 
Docker based web hosting
Docker based web hostingDocker based web hosting
Docker based web hostingDonghyeok Kang
 
Flutter Beta but Better and Better
Flutter Beta but Better and BetterFlutter Beta but Better and Better
Flutter Beta but Better and BetterDonghyeok Kang
 
워드프레스 플러그인 개발 입문
워드프레스 플러그인 개발 입문워드프레스 플러그인 개발 입문
워드프레스 플러그인 개발 입문Donghyeok Kang
 
[제1회 루씬 한글분석기 기술세미나] solr로 나만의 검색엔진을 만들어보자
[제1회 루씬 한글분석기 기술세미나] solr로 나만의 검색엔진을 만들어보자[제1회 루씬 한글분석기 기술세미나] solr로 나만의 검색엔진을 만들어보자
[제1회 루씬 한글분석기 기술세미나] solr로 나만의 검색엔진을 만들어보자Donghyeok Kang
 

Mais de Donghyeok Kang (8)

Divi custom post type template
Divi custom post type templateDivi custom post type template
Divi custom post type template
 
My second word press plugin
My second word press pluginMy second word press plugin
My second word press plugin
 
My first word press plugin
My first word press pluginMy first word press plugin
My first word press plugin
 
Docker based web hosting
Docker based web hostingDocker based web hosting
Docker based web hosting
 
Flutter Beta but Better and Better
Flutter Beta but Better and BetterFlutter Beta but Better and Better
Flutter Beta but Better and Better
 
워드프레스 플러그인 개발 입문
워드프레스 플러그인 개발 입문워드프레스 플러그인 개발 입문
워드프레스 플러그인 개발 입문
 
Curated News Platform
Curated News PlatformCurated News Platform
Curated News Platform
 
[제1회 루씬 한글분석기 기술세미나] solr로 나만의 검색엔진을 만들어보자
[제1회 루씬 한글분석기 기술세미나] solr로 나만의 검색엔진을 만들어보자[제1회 루씬 한글분석기 기술세미나] solr로 나만의 검색엔진을 만들어보자
[제1회 루씬 한글분석기 기술세미나] solr로 나만의 검색엔진을 만들어보자
 

Java Annotation과 MyBatis로 나만의 ORM Framework을 만들어보자

  • 1. Java Annotation과 MyBatis로 나만의 ORM Framework을 만들어보자 2011 JCO 11th Conference | Session ${track_#}-${session_#} | Javacommunity.Org 강동혁 (한솔헬스케어) wolfkang@gmail.com Revision: 20110612
  • 2. 하기 싫은 일 Class.forName("com.mysql.jdbc.Driver"); Connection c = DriverManager.getConnection(); PreparedStatement stmt = c.prepareStatement ("select title, year_made from movies " + "where year_made >= ?" + " and year_made < ?"); for(int decadeStart = 1920; decadeStart < 2000; decadeStart += 10){ stmt.setInt(1, decadeStart); stmt.setInt(2, decadeStart + 10); ResultSet rs = stmt.executeQuery(); while(rs.next()){ System.out.println(rs.getString(1) + " (" + rs.getInt(2) + ")"); } } 2
  • 3. 귀찮은 일 <insert id="insertDoctor" parameterType="domain.Doctor"> insert into Doctor (id,username,password,email,bio) values (#{id},#{username},#{password},#{email},#{bio}) </insert> <update id="updateDoctor" parameterType="domain.Doctor"> update Doctor set username = #{username}, password = #{password}, email = #{email}, bio = #{bio} where id = #{id} </update> <delete id="deleteDoctor” parameterType="int"> delete from Doctor where id = #{id} </delete> <select id="selectDoctor" resultType="domain.Doctor"> select * from Doctor </select> 3
  • 4. 머리 아픈 일 <hibernate-mapping> <class name="hello.Message“ table="MESSAGES"> <id name="id" column="MESSAGE_ID"> <generator class="increment"/> </id> <property name="text" column="MESSAGE_TEXT"/> <many-to-one name="nextMessage" cascade="all" column="NEXT_MESSAGE_ID"/> </class> </hibernate-mapping> SELECT new list(mother, offspr, mate.name) FROM DomesticCat AS mother INNER JOIN mother.mate AS mate LEFT OUTER JOIN mother.kittens AS offspr 4
  • 5. 하고 싶은 일 • SQL 작성 최소화 • Object oriented programming • 로직에 집중 • 단순한 설정 - 쉽고, 직관적 • 학습의 최소화 5
  • 8. 고려해야 할 점 • 설정 – object, relation 매핑 • SQL 생성 – object 를 SQL 의 파라미터로 전달 – SQL 결과를 object 로 리턴 8
  • 9. 구현 전략 • object 와 relation 의 1:1 매핑 구현 – MyBatis 를 이용하여 기본적인 insert, update, delete, select 문을 runtime 자동 생성 – 생성된 SQL문을 MyBatis SQL repository 에 저장 – Association 은 다루지 않음 – Join, subquery 구문은 MyBatis로 처리 • 설정은 Java Annotation 사용 • 일단 MySQL 먼저 9
  • 10. Java Annotation • 자바 5.0 에서 소개 • 자바 소스 코드에 추가되는 문법적인 메타데이터 • 클래스, 메소드, 변수, 파라미터, 패키지에 첨언하 여 컴파일러의 의해 .class 파일에 포함되어 컴파일 혹은 런타임 시 이용 • 번거로운 설정 작업들과 반복적인 코드를 줄여줌 • Built-in Annotations – @Override, @Deprecated, @SupressWarnings • Custom Annotations – @Controller, @Service, @Entity, @Column 10
  • 11. Custom Annotation • @interface 선언 public @interface MyAnnotation { String value(); } • Annotation 추가 import MyAnnotation @MyAnnotation(value=“my annotation”) public void myMethod(int arg) { // do something } 11
  • 12. Annotation API • Class 와 Field – getAnnotation(Class<A> annotationClass) 특정 타입에 대한 annotation 을 리턴 – getAnnotations() 모든 타입의 annotation 을 리턴 – isAnnotationPresent(Class annotationClass) 특정 타입에 대한 annotation 이 존재하면 true 리턴 12
  • 13. MyBatis • 구) iBatis • SQL문과 객체를 매핑 • SQL문을 XML 파일에 저장 • JDBC 코드 제거 13 출처) www.mybatis.org
  • 14. MyBatis 예제 • Mapped Statement <mapper namespace="org.mybatis.example.HospitalMapper"> <select id="selectHospital" parameterType="int" resultType="Hospital"> select * from Hospital where id = #{id} </select> </mapper> • Java DAO code public interface HospitalMapper { public Hospital selectHospital(int id); } HospitalMapper mapper = session.getMapper(HospitalMapper.class); Hospital hospital = mapper.selectHospital(101); 14
  • 15. 순서 1. Table 생성 2. Annotation 정의 3. Mapping class 생성 4. EntityManager 작성 – CRUD interface 5. SQL Generation 클래스 작성 – InsertSqlSource – UpdateSqlSource – DeleteSqlSource – SelectOneSqlSource – SelectListSqlSource 15
  • 16. 1. Table 생성 Create table hospital ( hospitalid INT NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(30), regdttm DATETIME ) 16
  • 17. 2. Annotation 정의 public @interface Table { String value() default ""; } public @interface Column { String name() default ""; boolean primaryKey() default false; boolean autoIncrement() default false; } 17
  • 18. 3. Mapping Class @Table(“hospital”) Public class Hospital { @Column(primaryKey=true, autoIncrement=true) private Integer hospitalid; @Column private String name; @Column(name=“regdttm”) private Date regdttm; } 18
  • 19. 4. EntityManager void insert(Object parameter) void update(Object parameter) void delete(Object parameter) Object load(Object parameter) List list(Object parameter) List list(Object parameter, String orderby) List list(Object parameter, String orderby, int rows) 19
  • 20. EntityManager 사용예 EntityManager entityManager = new EntityManager(); Hospital hospital = new Hospital(); hospital.setHospitalid(12345); hospital.setName(“JCO병원”); hospital.setRegdttm(new Date()); entityManager.insert(hospital); hospital = entityManager.load(hospital); hospital.setName(“JCO병원”); entityManager.update(hospital); entityManager.delete(hospital); 20
  • 21. 5. SQL Generation • INSERT – 컬럼 중 auto_increment 로 선언된 컬럼은 insert 문 에서 제외 • UPDATE – Primary Key 가 아닌 컬럼들만 update – PK로 where 조건절 생성 • DELETE – parameter 객체에서 null 값이 아닌 field 로 where 조 건절 생성 • SELECT – parameter 객체에서 null 값이 아닌 field 로 where 조 건절 생성 21
  • 22. 5-1. Insert문 생성 • 컬럼 중 auto_increment 로 선언된 컬럼은 insert 문에서 제외 <생성되는 SQL> INSERT INTO hospital (name, regdttm) VALUES (#{name}, #{regdttm}) 22
  • 23. EntityManager.insert() 1: Class<?> clazz = object.getClass(); 2: String statementName = PREFIX_INSERT + 3: clazz.getSimpleName(); 4: if (!configuration.hasStatement(statementName)) { 5: addMappedStatement( 6: statementName, 7: new InsertSqlSource(sqlSourceParser,clazz), 8: SqlCommandType.INSERT,null); 9: } 10: getSqlSession().insert(statementName, object); 23
  • 24. InsertSqlSource 1: List<String> columnNames = 2: AnnotationUtil.getNonAutoIncrementColumnNames(clazz); 3: String sql = String.format( 4: “INSERT INTO %1$s ( %2$s ) VALUES ( %3$s ) ", 5: AnnotationUtil.getTableName(clazz), 6: StringUtil.join(columnNames, ","), 7: StringUtil.join(columnNames, "#{%1$s}",",")); 8: parse(sqlSourceParser, sql, clazz); 24
  • 25. AnnotationUtil.getTableName Table t = clazz.getAnnotation(Table.class); return t.value(); 25
  • 26. AnnotationUtil.getNonAutoIncrem entColumnNames 1: List<String> names = new LinkedList<String>(); 2: for (Field field : clazz.getDeclaredFields()) { 3: if (field.isAnnotationPresent(Column.class)) { 4: Column c = field.getAnnotation(Column.class); 5: if (!c.autoIncrement()) 6: names.add("".equals(c.name()) ? field.getName():c.name()); 7: } 8: } 9: return names; 26
  • 27. 5-2. Update문 생성 • Primary Key 가 아닌 컬럼들만 update • PK로 where 조건절 생성 <생성되는 SQL> UPDATE hospital SET name = #{name}, regdttm = #{regdttm} WHERE hospitalid = #{hospitalid} 27
  • 28. EntityManager.update() 1: Class<?> clazz = object.getClass(); 2: String statementName = PREFIX_UPDATE + 3: clazz.getSimpleName(); 4: if (!configuration.hasStatement(statementName)) { 5: addMappedStatement( 6: statementName, 7: new UpdateSqlSource(sqlSourceParser,clazz), 8: SqlCommandType.UPDATE,null); 9: } 10: getSqlSession().update(statementName, object); 28
  • 29. UpdateSqlSource String sql = String.format( "UPDATE %1$s SET %2$s WHERE %3$s", AnnotationUtil.getTableName(clazz), StringUtil.join( AnnotationUtil.getNonPrimaryKeyColumnNames(clazz), "%1$s = #{%1$s}", ", "), StringUtil.join( AnnotationUtil.getPrimaryKeyColumnNames(clazz), "%1$s = #{%1$s}"," AND ")); parse(sqlSourceParser, sql, clazz); 29
  • 30. AnnotationUtil.getNonPrimaryKey ColumnNames 1: List<String> names = new LinkedList<String>(); 2: for (Field field : clazz.getDeclaredFields()) { 3: if (field.isAnnotationPresent(Column.class)) { 4: Column c = field.getAnnotation(Column.class); 5: if (!c.primaryKey()) 6: names.add("".equals(c.name()) ? field.getName() : 7: c.name()); 8: } 9: } 10: return names; 30
  • 31. AnnotationUtil.getPrimaryKeyColu mnNames List<String> names = new LinkedList<String>(); for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(Column.class)) { Column c = field.getAnnotation(Column.class); if (c.primaryKey()) names.add("".equals(c.name()) ? field.getName() : c.name()); } } return names; 31
  • 32. 5-3. Delete문 생성 • parameter 객체에서 null 값이 아닌 field 로 where 조건절 생성 예) Hospital hospital = new Hospital(); hospital.setName(“JCO병원”); entityManager.delete(hospital); <생성되는 SQL> DELETE FROM hospital WHERE name = #{name} 32
  • 33. EntityManager.delete() 1: Class<?> clazz = object.getClass(); 2: String statementName = PREFIX_DELETE + clazz.getSimpleName(); 3: if (!configuration.hasStatement(statementName)) { 4: addMappedStatement( 5: statementName, 6: new DeleteSqlSource(sqlSourceParser,clazz), 7: SqlCommandType.DELETE,null); 8: } 9: getSqlSession().delete(statementName, object); 33
  • 34. DeleteSqlSource 1: public DeleteSqlSource(SqlSourceBuilder sqlSourceParser, 2: Class<?> clazz) { 3: super(sqlSourceParser); 4: staticSql = "DELETE FROM” 5: +AnnotationUtil.getTableName(clazz); 6: } 7: public BoundSql getBoundSql(Object parameterObject) { 8: String sql = staticSql + " WHERE " + 9: StringUtil.join( 10: AnnotationUtil.getNotNullColumnNames(parameterObject), 11: "%1$s = #{%1$s}"," AND "); 12: return getBoundSql(sql,parameterObject); 13: } 34
  • 35. 5-4. Select문 생성 • parameter 객체에서 null 값이 아닌 field 로 where 조건절 생성 예) Hospital hospital = new Hospital(); hospital.setName(“JCO병원”); hospital = (Hospital)entityManager.load(hospital); <생성되는 SQL> SELECT hospitalid, name, regdttm FROM hospital WHERE name = #{name} 35
  • 36. EntityManager.load() 1: Class<?> clazz = object.getClass(); 2: String statementName = PREFIX_LOAD + clazz.getSimpleName(); 3: if (!configuration.hasStatement(statementName)) { 4: addMappedStatement(statementName, 5: new SelectOneSqlSource(sqlSourceParser,clazz), 6: SqlCommandType.SELECT,clazz); 7: } 8: Object result = getSqlSession().selectOne(statementName, object); 9: if (result != null) 10: BeanUtils.copyProperties(result, object); 11: return result; 36
  • 37. SelectOneSqlSource 1: public SelectOneSqlSource(SqlSourceBuilder sqlSourceParser, Class<?> clazz) { 2: super(sqlSourceParser); 3: staticSql = String.format("SELECT %1$s FROM %2$s ", 4: StringUtil.join(AnnotationUtil.getColumnNames(clazz),", "), 5: AnnotationUtil.getTableName(clazz)); 6: } 7: 8: public BoundSql getBoundSql(Object parameterObject) { 9: String sql = staticSql + " WHERE " + 10: StringUtil.join( 11: AnnotationUtil.getNotNullColumnNames(parameterObject), 12: "%1$s = #{%1$s}"," AND ") + 13: " LIMIT 1"; 14: return getBoundSql(sql,parameterObject); 15: } 37
  • 38. 복습 1. Table 생성 2. Annotation 정의 3. Mapping class 생성 4. EntityManager 작성 – CRUD interface 5. SQL Generation 클래스 작성 – InsertSqlSource – UpdateSqlSource – DeleteSqlSource – SelectOneSqlSource – SelectListSqlSource 38
  • 39. Effect • 비타민MD 사이트에 일부 적용 • 만약 전체 적용한다면 69% SQL문 제거 가능 39 SQL 적용 전 적용 후 insert 112 7 update 80 2 delete 79 0 select 327 176 총 598 185
  • 40. Future Work • Annotation 추가 – Like 구문 지원 – DDL 상의 default 값, not null 등의 constraint 지원 • Transaction • Caching • Oracle, MSSQL, DB2 등 다른 DBMS 지원 40
  • 42. Do It Yourself and Have Fun. 42
  • 43. 이 저작물은 크리에이티브 커먼스 코리아 저작자표시-비영리- 동일조건변경허락 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다. This work is Licensed under Creative Commons Korea Attribution 2.0 License.