server
# Project
v43_1 -> MyBatis SQL Mapper FrameWork 사용
// stmt.executeQuery("select board_id, titl from lms_board where board_id=no")
// stmt.executeQuery // JDBC 코드 // select board_id, titl from lms_board where board_id=no // SQL문
// JDBC programming 방식의 문제점 // 1. JDBC코드가 반복적으로 작성된다.
// 2. SQL문과 JDBC코드가 섞여있어 가독성이 떨어진다.
// SQL문이나 JDBC만 유지보수 하고싶어도 가독성이 떨어져 힘들다.
// 3. 개발자가 DBMS 따라 SQL문을 구분해서 다뤄야 한다. // DBMS마다 SQL문법이 다르다.
// 개발자가 SQL 문법을 다루기가 까다롭다.
// 해결책 // 1. SQL Mapper 사용 // Mybatis // persistence framework
// 1) 반복적으로 작성하는 JDBC코드를 캡슐화 // 코드 단순화
// 2) SQL 별도 파일로 분리 // 코드 가독성을 높힌다.
// 2. ORMapper // 객체관계맵퍼 // SI업체 처럼 제공하는 기능이 고정이 되어있을 때 // 자신이 직접 운영할 때
// 1) JDBC 코드를 캡슐화 한다. // SQL Mapper와 여기까지는 단계가 같다.
// 2) JDBC 코드를 DBMS에 맞춰 SQL문으로 변환을 한다. // 객체를 다루는 특별한 문법 // ex. HQL
// 장점 : DBMS에 따라 SQL을 변경할 필요가 없다. // SQL 몰라도 된다.
// 테이블의 데이터를 객체로 직접 다룰 수 있다. // 자바 개발자관점으로 데이터를 객체로 다룰 수 있어 편하다.
// 단점 : Framework에서 만든 전용 Query Language를 배워야 한다. // 결국 무언가를 배워야 한다.
// DSL(Domain Specific Language) // 특정 영역에서 사용되는 언어
// DSL을 해당 DBMS의 SQL로 변환할 수 있는 플러그인이 있어야 사용할 수 있다.
// 유명 DBMS가 아니면 플러그인을 구하기가 힘들다.
// com.eomcs.lms.conf 패키지 생성 // src/main/resource/ 경로에 만든다.
// resource 경로에는 실제 파일을 두고, java에는 java파일을 둔다.
# key=value
jdbc.driver=org.mariadb.jdbc.Driver
jdbc.url=jdbc:mariadb://localhost:3306/studydb
jdbc.username=study
jdbc.password=1111
// conf 패키지 밑에 jdbc.properties 파일 생성 // driver 경로, 접속 url, DB접속시 username password 정보를 입력
<configuration>
<properties resource="com/eomcs/lms/conf/jdbc.properties"></properties>
<typeAliases>
<typeAlias type="com.eomcs.lms.domain.Board" alias="Board"/>
</typeAliases>
<environments default="development">
</environments>
<mappers>
<mapper resource="com/eomcs/lms/mapper/BoardMapper.xml"/>
</mappers>
</configuration>
// mybatis-config.xml 생성 // environments는 추후 자세히 설명 // 내용 제외함.
// com.eomcs.lms.mapper 패키지 생성 // src/main/resource/ 경로에 만든다.
<mapper namespace="BoardMapper">
<resultMap type="Board" id="BoardMap">
<id column="board_id" property="no"/>
<result column="conts" property="title"/>
<result column="cdt" property="date"/>
<result column="vw_cnt" property="viewCount"/>
</resultMap>
</mapper>
// BoardMapper.xml 생성
<mapper namespace="PhotoBoardMapper">
<resultMap type="PhotoBoard" id="PhotoBoardMap">
<id column="photo_id" property="no"/>
<result column="titl" property="title"/>
<result column="cdt" property="createdDate"/>
<result column="vw_cnt" property="viewCount"/>
<association property="lesson" javaType="Lesson">
<id column="lesson_id" property="no"/>
<result column="lesson_titl" property="title"/>
</association>
<collection property="files" ofType="PhotoFile">
<id column="photo_file_id" property="no"/>
<result column="file_path" property="filepath"/>
</collection>
</resultMap>
// PhotoBoardMapper.xml 생성
// association은 객체 1개만 사용할 때 // lesson을 참조할 때, 한 개의 lesson만 불러온다.
// collection은 객체 여러개를 사용할 때 // PhotoFile은 첨부파일이 여러개일 수 있기 때문에 객체를 여러개 사용.
<insert id="insertBoard" parameterType="Board">
insert into lms_board(conts)
values(#{title})
</insert>
@Override
public int insert(Board board) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
int count = sqlSession.insert("BoardMapper.insertBoard", board);
sqlSession.commit();
return count;
}
}
// BoardMapper.xml에 insert() 기능 추가 // BoardDaoImpl에 insert() 기능 변경
<insert id="insertPhotoBoard" parameterType="PhotoBoard"
useGeneratedKeys="true" keyColumn="photo_id" keyProperty="no">
insert into lms_photo(titl,lesson_id)
values(#{title}, #{lesson.no})
</insert>
@Override
public int insert(PhotoBoard photoBoard) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
int count = sqlSession.insert(//
"PhotoBoardMapper.insertPhotoBoard", photoBoard);
sqlSession.commit();
return count;
}
}
// PhotoBoardMapper.xml에 insert() 기능 추가 // PhotoBoardDaoImpl에 insert() 기능 변경
// PhotoBoardMapper의 insert()에는 useGeneratedKeys를 설정한다. // Board, Lesson등 다른 Mapper에는 없다.
if (photoBoardDao.insert(photoBoard) == 0) {
throw new Exception("사진 게시글 등록에 실패했습니다.");
}
for (PhotoFile photoFile : photoFiles) {
photoFile.setBoardNo(photoBoard.getNo());
photoFileDao.insert(photoFile);
}
// PhotoBoardAddSevlet에 보면 insert한 후에, photoBoardDao의 no에 대한 값이 DB에서 생성된다.
// 즉 현재 가지고 있는 photoBoardDao에는 no에 대한 값이 없는 상태이다.
// 그러나 photoFile을 insert하기 전, photoFile에 BoardNo를 주어야 하는데, 가지고 있지 않아 줄 수가 없다.
// 따라서 photoBoardDao insert하고, useGeneratedKeys로 데이터를 받아와야 no를 넘겨줄 수 있는 것이다.
// 트랜잭션을 사용하고, 한 photoBoard에 여러개 photoFile이 필요하여 유일하게
// PhotoBoardMapper의 insert()에는 useGeneratedKeys를 설정하는 것이다.
<select id="selectPhotoBoard" resultMap="PhotoBoardMap" parameterType="int">
select
photo_id,
titl,
cdt,
vw_cnt
from lms_photo
where
lesson_id=#{no}
order by
photo_id desc
</select>
@Override
public List<PhotoBoard> findAllByLessonNo(int lessonNo) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectList(//
"PhotoBoardMapper.selectPhotoBoard", lessonNo);
}
}
// PhotoBoardMapper.xml에 findAll() 기능 추가 // PhotoBoardDaoImpl에 findAllByLessonNo() 기능 변경
<select id="selectDetail" resultMap="PhotoBoardMap" parameterType="int">
select
p.photo_id,
p.titl,
p.cdt,
p.vw_cnt,
l.lesson_id,
l.titl lesson_titl,
f.photo_file_id,
f.file_path
from lms_photo p
inner join lms_lesson l on p.lesson_id=l.lesson_id
left outer join lms_photo_file f on p.photo_id=f.photo_id
where
p.photo_id=#{no}
</select>
@Override
public PhotoBoard findByNo(int no) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectOne("PhotoBoardMapper.selectDetail", no);
}
}
// PhotoBoardMapper.xml에 detail() 기능 추가 // PhotoBoardDaoImpl에 detail() 기능 변경
// 조인기능을 사용한다. // 일부로 어려운 PhotoBoard를 가져왔음.
<update id="updatePhotoBoard" parameterType="PhotoBoard">
update lms_photo set
titl=#{title}
where photo_id=#{no}
</update>
@Override
public int update(PhotoBoard photoBoard) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
int count = sqlSession.update(//
"PhotoBoardMapper.updatePhotoBoard", photoBoard);
sqlSession.commit();
return count;
}
}
// PhotoBoardMapper.xml에 update() 기능 추가 // PhotoBoardDaoImpl에 update() 기능 변경
<delete id="deletePhotoBoard" parameterType="int">
delete from lms_photo
where photo_id=#{no}
</delete>
@Override
public int delete(int no) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
int count = sqlSession.delete("PhotoBoardMapper.deletePhotoBoard", no);
sqlSession.commit();
return count;
}
}
// PhotoBoardMapper.xml에 delete() 기능 추가 // PhotoBoardDaoImpl에 delete() 기능 변경
v43_2 -> MyBatis + 트랜잭션 적용
// 현재 MyBatis를 적용하면서 기존의 트랜잭션이 현재 사용되지 않는 상태이다.
// 사진 파일을 등록할 때, 오류가 발생하도록 긴 파일명을 입력해보면 알 수 있다.
// 오류가 발생하기 전 입력한 것들은 정상적으로 commit이 된 상태이다.
// 트랜잭션이 정상적으로 돌아갔다면, commit이 되면 안된다 // 오류 발생으로 rollback 했어야 한다.
// 문제 발생 이유 // MyBatis의 SQLSession이 자체적으로 커넥션을 관리하기 때문에
// SqlSession sqlSession = sqlSessionFactory.openSession() // DaoImpl에서 각 메서드가 호출한다.
// DaoImpl가 openSession을 통해 MyBatis의 트랜잭션을 관리하는 '새 커넥션'을 얻기 때문이다.
// 즉 Dao가 매번 커넥션을 새로 얻게 하지 않게끔 해야 한다.
// com.eomcs.sql.SqlSessionProxy // 클래스 추가 // implements SqlSession
// close()를 호출할 때 닫지 않도록 오버라이딩 // realClose() 생성
// com.eomcs.sql.SqlSessionFactoryProxy // 클래스 추가 // implements SqlSessionFactory
// ThreadLocal sqlSessionLocal = new ThreadLocal<>();
// SqlSession 객체를 스레드에 보관하기 위해 ThreadLocal 필드를 추가
public void closeSession() {
SqlSession sqlSession = sqlSessionLocal.get();
if (sqlSession != null) {
sqlSessionLocal.remove();
((SqlSessionProxy) sqlSession).realClose();
}
}
@Override
public SqlSession openSession() {
return this.openSession(true);
}
@Override
public SqlSession openSession(boolean autoCommit) {
SqlSession sqlSession = sqlSessionLocal.get();
if (sqlSession == null) {
sqlSession = new SqlSessionProxy(originalFactory.openSession(autoCommit));
sqlSessionLocal.set(sqlSession);
}
return sqlSession;
}
// closeSession() // 메서드 추가 Session을 실제로 닫는,
// SqlSessionProxy에서 만든 realClose()를 호출하는 메서드를 만든다.
// openSession() // 기본으로 자동 커밋으로 동작하는 SqlSession을 만들어 리턴한다.
// openSession(boolean) // 스레드에 보관된 게 없다면 새로 만든다. // 스레드에 보관한다.
// com.eomcs.sql.PlatformTransactionManager // SqlSessionFactory을 생성자로 받는다.
// 기존에는 DataSource의 Connection를 가지고 트랜잭션을 다루었지만,
// SqlSession에 트랜잭션을 다루는 기능이 있기 때문에, SqlSession을 사용할 예정이다.
// PlatformTransactionManager의 역할은 트랜잭션을 다루는 것이므로, SqlSessionFactory를 받는다.
public void beginTransaction() throws Exception {
((SqlSessionFactoryProxy) sqlSessionFactory).closeSession();
sqlSessionFactory.openSession(false);
}
public void commit() throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
sqlSession.commit();
}
public void rollback() throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
sqlSession.rollback();
}
// beginTransaction()은 트랜잭션을 사용하기전에
// Session이 이미 만들어져 있으면 닫고 새로 Session(false)를 만들어 사용한다는 의미이다.
// 기존 Session을 닫고, 새로 Session을 false로 만들어 쓰는 이유
// photoboardadd를 실행하면, LessonDaoImpl가 가장 먼저 실행된다. // 수업 번호를 물을 때 사용
// LessonDaoImpl에서 Session을 만드는데, 이 Session은 True로 고정이 되어있어, 트랜잭션을 사용할 수 없다.
// 트랜잭션을 사용하려면 Session을 무조건 commit을 수동으로 할 수 있게 false여야만 한다.
// com.eomcs.sql.DataLoaderListener 변경 // jdbcUrl, username, password, dataSource 삭제한다.
// new SqlSessionFactoryProxy(new SqlSessionFactoryBuilder().build(inputStream));
// SqlSessionFactory sqlSessionFactory를 SqlSessionFactoryProxy로 만든다.
// DaoImpl에 sqlSessionFactory를 넘겨준다. // dataSource는 아예 사용하지 않기 때문이다.
// context.put("sqlSessionFactory", sqlSessionFactory); // ServerApp에서 꺼내서 Session을 닫아야 하기에 넣는다.
// com.eomcs.sql.DataSource // ConnectionProxy // 삭제 // 안쓴다.
// ServerApp 변경 // ((SqlSessionFactoryProxy) sqlSessionFactory).closeSession(); // 실제 Session() 닫는 코드 추가
// DaoImpl 변경 // commit() rollback() 호출 코드 제거한다. // Dao에서 관리하지 않는다.
v43_3 -> MyBatis의 dynamic sql 문법 사용하기
// Mapper 파일에 <sql> 적용 //
<sql id="select1">
select
board_id,
conts,
cdt,
vw_cnt
from
lms_board
</sql>
<select id="selectBoard" resultMap="BoardMap">
<include refid="select1"/>
order by
board_id desc
</select>
// sql문을 이용해 중복되는 파트를 추출하고, <inclue refid="select1"/>를 적용하였다.
// 중복되는 파트가 만약, 중간파트라면 vw_cnt 뒤에도 ,가 있어야 한다. // Board는 그렇지 않으므로 뒤에 ,가 없다.
// PhotoFileMapper에 <foreach> 적용 //
<insert id="insertPhotoFile" parameterType="PhotoBoard">
insert into lms_photo_file(photo_id,file_path)
values
<foreach collection="files" item="file" separator=",">
(#{no}, #{file.filepath})
</foreach>
</insert>
// PhotoFileMapper는 PhotoBoard 하나에 여러개의 첨부파일이 들어갈 수 있기 때문에
// <foreach> 태그를 적용하여 여러 개의 값을 한 번에 insert 한다.
// dynamic sql 문법 사용 전에는 PhotoBoard 만들고, PhotoFiles를 담을 list를 별도로 만들어서
// PhotoBoardDaoImpl에게 insert 요청하면서 파라미터로 PhotoBoard를 건내줬고,
// PhotoFileDaoImpl에게 insert 요청하면서 파라미터로 PhotoFile(List)을 건내줬다.
// PhotoBoard에는 기존에 PhotoFiles를 받을 수 있는 컬럼이 이미 있다.
// PhotoBoardDaoImpl에게 insert 요청하면서 파라미터로 PhotoBoard를 건내주고,
// PhotoFileDaoImpl에게 insert 요청하면서 파라미터로 PhotoBoard를 건내줄 수 있는 것이다.
// PhotoFileDaoImpl가 PhotoBoard에서 File를 Collection으로 받아 사용하면 된다.
// photoBoard.setFiles(photoFiles); // PhotoBoardAddServlet 변경
// photoFiles를 for문으로 photoFileDao에게 insert하는 코드는 지워준다 // foreach가 대신하기 때문
// PhotoBoardUpdateServlet 변경 // printPhotoFiles 메서드 변경
// PhotoBoardUpdateServlet의 기존 작동 순서 // 번호 입력받음 - photoBoardDao이용 해당 PhotoBoard 찾음
// - 해당 Board의 첨부파일 목록 출력 메서드(printPhotoFiles) photoFileDao 이용 - 전체 첨부 파일 불러옴
// 따라서 첨부파일 목록을 불러오기 전에, 트랜잭션을 실행하여야 했다. // Dao를 두개 사용하기 때문
// 이제는 PhotoBoard가 Files를 가지고 있기 때문에, 해당 PhotoBoard 안의 첨부파일을 불러오면 된다.
// 즉, photoFileDao를 이용하지 않아도 되기 때문에, 첨부파일을 불러오기 전에 트랜잭션을 실행할 필요가 없다.
// 사용자가 첨부파일을 정상적으로 다 업데이트 했다고 알림을 보내면, 그때 트랜잭션을 실행
// photoBoardDao.update(photoBoard) // photoFileDao.deleteAll(no) // photoFileDao.insert(photoBoard);
// Dao에게 photoBoard를 넘긴다. // photoFileDao는 기존 첨부파일을 다 지우고 insert 해야 한다.
// PhotoFileDao 변경 // insert시, PhotoFile을 받다가, PhotoBoard를 받기 때문에 변경해준다.
// PhotoBoardDaoImpl 변경 // 마찬가지로 PhotoBoard 객체를 넘겨주기 때문에 변경한다.
// LessonMapper, MemberMapper에 <set> 적용 // update에 set을 적용한다.
<update id="updateLesson" parameterType="Lesson">
update lms_lesson
<set>
<if test="title != null and title != ''">titl=#{title},</if>
<if test="description != null and description != ''">conts=#{description},</if>
<if test="startDate != null">sdt=#{startDate},</if>
<if test="endDate != null">edt=#{endDate},</if>
<if test="totalHours > 0">tot_hr=#{totalHours},</if>
<if test="dayHours > 0">day_hr=#{dayHours}</if>
</set>
where lesson_id=#{no}
</update>
// Prompt 변경 // 사용자가 정상적인 값을 입력하지 않으면 0이나 null 리턴하도록 변경
// update시 반영이 안되도록 수정한다.
// LessonUpdateServlet, MemberUpdateServlet 변경 // 값을 입력하지 않았을 때, 이전 값을 넣도록 변경
// LessonMapper에 <where> 적용 //
<select id="selectLesson" resultMap="LessonMap" parameterType="map">
<include refid="select1"/>
from lms_lesson
<where>
<if test="title != null">titl like concat('%', #{title}, '%')</if>
<if test="startDate != null">and sdt >= #{startDate}</if>
<if test="endDate != null">and edt <= #{endDate}</if>
<if test="totalHours != null">and tot_hr <= #{totalHours}</if>
<if test="dayHours != null">and day_hr <= #{dayHours}</if>
</where>
</select>
// select를 수정하여 키워드를 입력했을 때, 해당 조건을 검색하게 조건 추가
// LessonSearchServlet 추가 // List lessons = lessonDao.findByKeyword(params);
// keyword를 파라미터에 담아 selectLesson에게 전달한다.
// selectLesson은 Map에서 해당하는 컬럼의 값이 있는지 여부에 따라 조건이 추가되게 작동한다.
// LessonDao 변경 // findByKeyword() 메서드 추가 // ServerApp // LessonSearchServlet 등록
v44_1 -> UI 객체에서 비즈니스 로직 분리하기
// Presentation Layer // UI를 담당한다.
// Business(Service) Layer // 업무 로직을 담당 & 트랜잭션 제어를 담당한다.
// Persistence Layer // 데이터 저장을 담당한다.
// 기존에 Servlet이 고객한테 보여주는 UI도 담당하고, 업무 로직을 담당 & 트랜잭션 제어를 담당했다.
// Servlet에게 이제 UI의 담당하는 역할만 남기고
// 업무 로직을 담당 & 트랜잭션 제어를 담당하는 역할을 Service에게 넘기는 것이다.
// Presentation Layer와 Business(Service) Layer를 분리
// com.eomcs.lms.service.*Service 생성 // 각 Service를 생성한다. // add, list, delete, get, update
// com.eomcs.lms.service.impl.*ServiceImpl // 각 인터페이스를 구현하는 Impl를 생성한다.
// PhotoFile의 경우 Client한테 보여지는 UI가 없기 때문에 Service를 만들 이유가 없다. // 첨부파일이라
// PhotoBoard처럼 트랜잭션을 사용하는 메서드를 제외하고 *ServiceImpl에서 구현할게 없다.
// *DaoImpl에 모든 기능을 다 구현해 두었기 때문에, 해당 기능을 호출만 하면 된다.
public class BoardServiceImpl implements BoardService {
BoardDao boardDao;
public BoardServiceImpl(BoardDao boardDao) {
this.boardDao = boardDao;
}
@Override
public void add(Board board) throws Exception {
boardDao.insert(board);
}
@Override
public List<Board> list() throws Exception {
return boardDao.findAll();
}
@Override
public int delete(int no) throws Exception {
return boardDao.delete(no);
}
@Override
public Board get(int no) throws Exception {
return boardDao.findByNo(no);
}
@Override
public int update(Board board) throws Exception {
return boardDao.update(board);
}
// *Dao가 DB에다가 SQL문으로 넘기는 작업을 하기 때문에, Dao만 생성자로 받고 Dao에게 해당 작업을 넘긴다.
// 파라미터 받은거 그대로 넘겨주면서 짬처리 한다.
public class PhotoBoardServiceImpl implements PhotoBoardService {
TransactionTemplate transactionTemplate;
PhotoBoardDao photoBoardDao;
PhotoFileDao photoFileDao;
public PhotoBoardServiceImpl(PlatformTransactionManager txManager, PhotoBoardDao photoBoardDao, PhotoFileDao photoFileDao) {
this.transactionTemplate = new TransactionTemplate(txManager);
this.photoBoardDao = photoBoardDao;
this.photoFileDao = photoFileDao;
}
@Override
public void add(PhotoBoard photoBoard) throws Exception {
transactionTemplate.execute(new TransactionCallback() {
@Override
public Object doInTransaction() throws Exception {
if (photoBoardDao.insert(photoBoard) == 0) {
throw new Exception("사진 게시글 등록에 실패했습니다.");
}
photoFileDao.insert(photoBoard);
return null;
}
});
}
@Override
public List<PhotoBoard> listLessonPhoto(int lessonNo) throws Exception {
return photoBoardDao.findAllByLessonNo(lessonNo);
}
@Override
public PhotoBoard get(int no) throws Exception {
return photoBoardDao.findByNo(no);
}
@Override
public void update(PhotoBoard photoBoard) throws Exception {
transactionTemplate.execute(() -> {
if (photoBoardDao.update(photoBoard) == 0) {
throw new Exception("사진 게시글 변경에 실패했습니다.");
}
if (photoBoard.getFiles() != null) {
photoFileDao.deleteAll(photoBoard.getNo());
photoFileDao.insert(photoBoard);
}
return null;
});
}
@Override
public void delete(int no) throws Exception {
transactionTemplate.execute(() -> {
photoFileDao.deleteAll(no);
if (photoBoardDao.delete(no) == 0) {
throw new Exception("해당 번호의 사진 게시글이 없습니다.");
}
return null;
});
}
}
// Servlet의 역할은 CLient에게 UI를 띄우고, Dao의 역할은 DB와(정확하게는 mybatis와) 연락을 한다.
// 따라서 중간에서 발생하는 트랜잭션 처리는 service가 담당하여야 한다.
// 트랜잭션을 사용하려면 transactionTemplate에게 트랜잭션을 받아와야 하고,
// PhotoBoardDao와 PhotoFileDao를 생성자로 받아, 사용하여야 한다.
// *Servlet 변경 // 일반적인 Servlet의 경우 *Dao에서 *Service를 넘겨받고, 그에 맞는 호출방법으로 바꿔준다.
// PhotoBoardAddServlet과 UpdateServlet의 경우에는 트랜잭션 코드를 Service로 옮겨두었다.
// PhotoBoardAddServlet는 PhotoBoard를 등록할 때 lesson_no가 필요하기 때문에 LessonService를 받아야한다.
// 실제 작업은 Dao가 하지만, Servlet은 Service를 호출하고 Service가 Dao를 호출하기 때문에
// PhotoBoardAddServlet에서는 LessonService를 호출하여야 한다.
// DataLoaderListener 변경 // servlet에서 사용할 서비스 객체를 준비한다.
// DataLoaderListener에서 DB연결을 위한 InputStream을 준비, 이용 - sqlSessionFactory를 준비, 이용 -
// Dao 및 txManager 준비, 이용 - Service를 준비하는 것이다.
// Servlet을 준비하여 넘길 수도 있지만, DataLoader라는 특성과는 맞지 않아서 ServerApp에서 준비한다.
// ServerApp 변경 // Servlet에 Dao 대신 Service를 꺼내서 넣어준다.
v45_1 -> Proxy를 이용하여 DAO 구현체 자동 생성
// XxxMapper.xml 변경 //
// mapper namespace="com.eomcs.lms.dao.BoardDao" // namespace를 경로&파일명과 일치시킨다.
// select id="findAll" resultMap="BoardMap" // 작업을 해당 경로&파일명의 메서드명과 일치시킨다.
// findByEmailAndPassword(Map<String, Object> params) // 파라미터를 여러개 넘기려면 만들기 복잡해진다.
// 딥하게 들어가서 만들 게 아니기 때문에, 파라미터를 한개만 넘기는 걸로 수정하기 위해 Map을 넘긴다.
// MemberServiceImpl // MemberDao의 파라미터가 Map으로 변경되었으니, Service도 바꾸어준다.
// ServiceImpl에서 Dao로 넘겨주기 때문에 Map 객체로 만들어서 넘겨주면 된다. // 파라미터까지 바꿀필요 없다.
// MybatisDaoFactory 추가 // Dao 생성을 자동화 시키는 일을 담당할 Class 이다.
public <T> T createDao(Class<T> daoInterface) {
return (T) Proxy.newProxyInstance(//
this.getClass().getClassLoader(), //
new Class[] {daoInterface}, //
invocationHandler);
}
// createDao 메서드 추가 // java.lang.reflect.Proxy 패키지 사용
// newProxyInstance(구현체를 만들기 위해 사용하는 인터페이스의 클래스로더, 구현할 인터페이스 정보 목록,
// 실제 작업을 수행하는 객체)
// 구현체를 만들기 위해 사용하는 인터페이스의 클래스로더는 우선,
// Proxy의 경우 new 인스턴스로 생성하지않는다. // protected로 막아두었다. // 이유는 정확히 모르지만
// Proxy를 전혀 상관없는 클래스에서 마음대로 호출하지 못하게 했다는 의미로 이해를 하자.
// 넘겨줄 클래스 로더는 어떤 클래스의 로더를 넘겨줘도 상관이 없다.
// ClassLoader는 그 클래스를 불러다가 Loading 시키는 역할을 하기 때문에,
// 어떤 클래스의 Loader이던 호출해도 그놈이 그놈이고 같은 놈이다. // 한 놈이 일을 하는거라 상관이 없는 것
// 구현할 인터페이스 정보 목록은 어떤 인터페이스를 구현할지 당연히 설계도를 넘겨야 한다.
// 실제 작업을 수행하는 객체는 InvocationHandler를 말하는데 여기서 참 이해가 오래걸렸다.
// InvocationHandler에서 Invoke 메서드는 Proxy를 파라미터로 필요로 하는데,
// Proxy는 InvocationHandler 구현 객체를 필요로 하기 때문이다.
// InvocationHandler구현체에서 Invoke를 실행하려면 Proxy가 필요한데,
// Proxy를 만드려면 InvocationHandler가 필요하니 이해가 안되는 것이였다.
이해 // newProxyInstance는 메서드고, InvocationHandler를 넘겨 받는다.
// Invoke 할 때, Proxy가 필요한 거지, InvocationHandler를 구현할때에는 필요가 없다.
// newProxyInstance에서 Proxy를 먼저 만든다. 넘겨받은 Interface 정보를 통해,
// 구현체(Impl)의 생성자의 파라미터 등 객체를 만들때 필요한 정보만 알면 되기 때문이다.
// newProxyInstance메서드 내에서 만들어진 Proxy와 사용자가 실행하겠다고 입력한 method명과
// Servlet이 Service를 호출하면서 파라미터를 넘겨주고(Client가 입력한 것)
// Service가 Dao를 호출하면서 파라미터로 넘겨준 애를 args에 보관하고 있다가 Invoke시 넘긴다.
invocationHandler = (proxy, method, args) -> {
Class<?> clazz = proxy.getClass();
Class<?> daoInterface = clazz.getInterfaces()[0];
String interfaceName = daoInterface.getName();
String methodName = method.getName();
String sqlId = String.format("%s.%s", interfaceName, methodName);
System.out.printf("SQL ID => %s\n", sqlId);
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
Class<?> returnType = method.getReturnType();
if (returnType == List.class) {
return (args == null) ? sqlSession.selectList(sqlId) : //
sqlSession.selectList(sqlId, args[0]);
} else if (returnType == int.class || returnType == void.class) {
return (args == null) ? sqlSession.update(sqlId) : // update()는 insert(), delete() 과 같다.
sqlSession.update(sqlId, args[0]);
} else {
return (args == null) ? sqlSession.selectOne(sqlId) : //
sqlSession.selectOne(sqlId, args[0]);
}
}
};
// (만들어진) 파라미터로 넘어온 Proxy의 정보를 알아낸다.
// 프록시 객체가 구현한 인터페이스 정보를 알아낸다. // getInterfaces()의 리턴 객체는 배열이다. // [0]번 호출
// 인터페이스 명을 가져온다. // 호출된 메서드 명을 가져온다.
// sqlId를 만든다. // XxxMapper.xml 변경한 이유가 여기서 나온다. // SqlSession을 얻는다.
// 호출된 메서드 명의 리턴 값을 가져온다. // MyBatis를 통해 데이터를 넘기고 리턴 값을 받는다.
// Dao 객체 제거 //
// DataLoaderListener 변경 //
MybatisDaoFactory daoFactory = new MybatisDaoFactory(sqlSessionFactory);
LessonDao lessonDao = daoFactory.createDao(LessonDao.class);
BoardDao boardDao = daoFactory.createDao(BoardDao.class);
MemberDao memberDao = daoFactory.createDao(MemberDao.class);
PhotoBoardDao photoBoardDao = daoFactory.createDao(PhotoBoardDao.class);
PhotoFileDao photoFileDao = daoFactory.createDao(PhotoFileDao.class);
// MybatisDaoFactory에 사용할 sqlSessionFactory를 넘기고, 만들 daoFactory에 인터페이스를 넘기고 제작한다.
v45_2 -> Mybatis를 이용하여 DAO 구현체 자동 생성하기
// BoardServiceImpl2 추가 //
// BoardDao boardDao = sqlSession.getMapper(BoardDao.class); // 한 줄 코드 추가로 생성 끝
// DataLoaderListener에서 BoardService 구현체를 변경하고 사용 해보기 //
v46_1 -> IoC 컨테이너 적용 // 객체 생성 및 등록을 자동화하는 객체
// IoC(Inversion Of Control) // 제어의 역전 //
// 1. 의존 객체 생성 // 객체를 사용하는 쪽에서 그 객체를 만드는 것이다.
// ex) 쌀을 이용할 사람들이 쌀 농사를 짓는다. // 옷을 사용할 사람들이 옷을 만든다.
// 시스템 구조가 복잡해지면 직접 객체를 만드는 방식이 비효율적이 된다. // 외부에서 주입 받아 사용한다.
// 이렇게 객체를 외부에서 주입하는 것은 보통의 실행 흐름을 역행하는 것이다. // 이런 흐름 역행을 IoC라 한다.
// 2. 메서드 호출 // 보통 메서드를 만들면 실행 흐름에 따라 호출하고 실행이 끝나면 리턴한다.
// 실행 계획에 따라 호출하는 것이 아니라 특정 상태에 있을 때 자동으로 호출되게 하는 경우도 필요하다.
// 시스템이 시작될 때, 사용자가 마우스를 클릭했을 때 특정 메서드를 자동으로 호출되게 하는 것.
// 작성한 코드 흐름에 따라 호출하는 것이 아니라, 특정 상태에 놓여졌을 때 뒤에서 자동으로 호출하는 방식
// '이벤트 핸들러', '이벤트 리스너' '콜백(callback) 메서드' // 라 한다.
// IoC 컨테이너 // 개발자가 직접 객체를 생성하지 않는다. // 객체 생성을 전담하는 역할자를 통해 객체가 준비
// 이 역할자를 '빈 컨테이너(bean container)'라고 부른다.
// 객체 스스로 자신이 사용할 객체를 만드는 것이 아니라, 외부의 빈 컨테이너로부터 의존 객체를 주입받는 것이다
// IoC 컨테이너 = 빈 컨테이너 + 의존 객체 주입
// com.eomcs.util.ApplicationContext // 클래스 생성
// 클래스를 찾아 객체 생성 // 객체가 일을 하는데 필요로 하는 의존 객체를 주입// 객체를 생성과 소멸을 관리
public ApplicationContext(String packageName, HashMap<String, Object> beans) throws Exception {
Set<String> keySet = beans.keySet();
for (String key : keySet) {
objPool.put(key, beans.get(key));
}
File path = Resources.getResourceAsFile(packageName.replace('.', '/'));
findClasses(path, packageName);
for (Class<?> clazz : concreteClasses) {
try {
createObject(clazz);
} catch (Exception e) {
System.out.printf("%s 클래스의 객체를 생성할 수 없습니다.\n", //
clazz.getName());
}
}
}
// 생성자로 Map을 받는다. // ContextLoaderListener에서 객체를 생성한 beans의 목록을 넘겨받는다.
// 넘겨받은 Map에서 key를 꺼내 objPool에 담는다. // ApplicationContext에서 추가적으로 사용하기 위함.
// ContextLoaderListener에서 만든 beans에서 따로 로딩 된 객체 검색하고,
// ApplicationContext에서 만든 objPool에서 따로 로딩된 객체를 검색하기엔 매우 비효율 적이기 때문
// 넘겨받은 beans를 ApplicationContext에 objPool에 다 넣고, 필요한 것을 추가적으로 만들기 위해,
// 넘겨받은 packageName에서 경로를 수정한다. // .을 /로 바꾸어준다.
// findClasses(path, packageName); // 해당 경로에서 모든 클래스 이름을 파악하는 메서드를 추가한다.
private void findClasses(File path, String packageName) throws Exception {
File[] files = path.listFiles(file -> {
if (file.isDirectory() //
|| (file.getName().endsWith(".class")//
&& !file.getName().contains("$")))
return true;
return false;
});
for (File f : files) {
String className = String.format("%s.%s", //
packageName, //
f.getName().replace(".class", ""));
if (f.isFile()) {
Class<?> clazz = Class.forName(className);
if (isComponentClass(clazz)) {
concreteClasses.add(clazz);
}
} else {
findClasses(f, className);
}
}
}
// .을 /로 바꿔준 path에서 listFiles를 뽑아낸다. // FileFilter()를 적용하는데, 오버라이딩하여 적용한다.
// 람다문법이며, FileFilter()를 적용했을때, 필터에 맞는(true)면 값을 리스트에 담는다는거 생각해서 보면 된다.
// isDirectory() 폴더이거나 // file.getName().endsWith(".class") 파일이 .class로 끝나면서,
// !file.getName().contains("$") 파일명에 $가 들어가지 않은 (이너 클래스)를 제외한 목록을 담는다.
// com.eomcs.lms.dao.BoardDao // classOrPackageName에 경로와 패키지 명을 나타내게 한다.
// 만든 files에서 꺼낸 값이 file이라면 꺼내서 해당 class가 isComponentClass인지 확인한다.
// 파일이 아니면 디렉토리이기 때문에 재귀호출로 패키지 타고타고타고타고 들어가서 파일을 부르게 만든다.
// isComponentClass 리턴 값이 true라면 concreateClasses에 목록을 추가한다.
// ArrayList<Class<?>> concreteClasses = new ArrayList<>(); // 클래스 변수로 선언한다. // 타 메서드에서 사용
private boolean isComponentClass(Class<?> clazz) {
if (clazz.isInterface() // 인터페이스인 경우
|| clazz.isEnum() // Enum 타입인 경우
|| Modifier.isAbstract(clazz.getModifiers()) // 추상 클래스인 경우
) {
return false; // 이런 클래스를 객체를 생성할 수 없다.
}
// 클래스에서 @Component 애노테이션 정보를 추출한다.
Component compAnno = clazz.getAnnotation(Component.class);
if (compAnno == null) {
return false;
}
// 오직 @Component 애노테이션이 붙은 일반 클래스만이 객체 생성 대상이다.
return true;
}
// 위 작업을 하려면 원래 사용할 Servlet에 애노테이션을 먼저 붙혀야 한다. // 이건 글 작성 편의상 뒤에 적음
// Interface와 Enum, Abstract 클래스 인지 확인한다.
// @Component 애노테이션이 붙었나 여부를 파악해서 저장소에 담는다.
// Component가 붙지 않은 Class의 객체는 생성 대상이 아니다.
// Component가 붙지 않았는데, 객체를 생성 해야되는 것은, 외부에서 작업해서 Bean에 넣는다.
// 즉 Component가 붙은 Class의 객체만 ApplicationContext에서 만들고 그 외의 객체 생성은
// 필요에 의해 다른 Class에서 생성하고 넣을 것이다.
// ArrayList<Class<?>> componentClasses = new ArrayList<>(); // 클래스 변수로 선언
private Object createObject(Class<?> clazz) throws Exception {
Constructor<?> constructor = clazz.getConstructors()[0];
Parameter[] params = constructor.getParameters();
Object[] paramValues = getParameterValues(params);
Object obj = constructor.newInstance(paramValues);
objPool.put(getBeanName(clazz), obj);
return obj;
}
// 넘어온 Class의 생성자를 호출하고, 생성자를 호출하기 위한 파라미터 정보를 받는다.
// 파라미터 정보를 통해 파라미터의 객체를 생성하기 위한 getParameterValues를 호출한다.
// getParameterValues에게 넘겨준 params는 파라미터가 어떠어떠한게 필요하다고 담아져 있는 정보이다.
// 아직 그 값이 들어 있는 것은 아니며, 해당 값이 들어있는 애를 얻기 위해 getParameterValues를 호출하는 것.
private Object[] getParameterValues(Parameter[] params) throws Exception {
Object[] values = new Object[params.length];
for (int i = 0; i < values.length; i++) {
values[i] = getParameterValue(params[i].getType());
return values;
}
// 넘어온 파라미터배열에서 각 파라미터의 타입을 얻은 뒤, getParameterValue를 호출한다.
// Object[] values는 getParameterValue가 값을 하나하나씩 가져올 때 담아둘 바구니라고 생각.
// 실제 파라미터에 들어갈 값을 구하거나 만드는 작업은 getParameterValue가 한다.
private Object getParameterValue(Class<?> type) throws Exception {
Collection<?> objs = objPool.values();
for (Object obj : objs) {
if (type.isInstance(obj)) {
return obj;
}
}
Class<?> availableClass = findAvailableClass(type);
if (availableClass == null) {
return null;
}
return createObject(availableClass);
}
// 파라미터가 어떤 class의 타입인지를 파라미터로 받아왔다. // int, String, Dao 등 다양한 타입이 넘어온다.
// 필요로 하는 파라미터가 이미 만들어져서 objPool에 담겨 있는지 확인을 한다 // 있으면 리턴
// 없다면 아직 만들어지지 않은 객체이거나 만들 수 없다는 것을 의미한다. // 인터페이스라면 객체 생성 불가능
// 없다면 objPool에 없다는 이야기 이므로 findAvailableClass 메서드를 호출한다.
// 파라미터로 넘겨주는 값은 만들어야 하는 파라미터의 클래스 Type 이다.
private Class<?> findAvailableClass(Class<?> type) throws Exception {
for (Class<?> clazz : concreteClasses) {
if (type.isInterface()) {
Class<?>[] interfaces = clazz.getInterfaces();
for (Class<?> interfaceInfo : interfaces) {
if (interfaceInfo == type) {
return clazz;
}
}
} else if (isChildClass(clazz, type)) {
return clazz;
}
}
return null;
}
// concreteClasses 목록에서 파라미터에 해당하는 클래스가 있는지 조사한다.
// concreteClasses 목록의 경우 해당 경로 패키지 및 하위 경로에서 concreteclass 목록을 뽑아낸 것이다.
// 만약 파라미터로 넘어온 Type이 Interface라면, clazz는 concreteClass 즉 구현체일 것이므로,
// clazz의 Interface들을 불러온다. // clazz의 Interface와 Type이 일치하면,
// clazz는 Interface Type을 구현한 구현체가 된다. // return clazz;
// 만약 파라미터로 넘어온 Type이 인터페이스가 아니라면,
// isChildClass 메서드를 호출한다.
private boolean isChildClass(Class<?> clazz, Class<?> type) {
if (clazz == type) {
return true;
}
if (clazz == Object.class) {
return false;
}
return isChildClass(clazz.getSuperclass(), type);
}
// type이 clazz와 같으면 return 한다. // clazz의 상위 클래스가 Object가 될 때까지 재귀 호출로 확인한다.
// 재귀호출로 모든 concreteClass와 비교했을 때 false를 리턴하면 getParameterValues에 null을 리턴한다.
// null 리턴 시 아무일도 일어나지 않고 // true를 리턴 시 getParameterValue에서 createObject()를 호출한다.
// 결국 게속 반복하다보면 createObject의 Object[] pramaValue에 리턴되는 clazz들이 모여 있게 된다.
// Object obj = constructor.newInstance(paramValues); // paramValues 안에 해당 객체를 실제로 생성한다.
// newInstance 실행 전까지는 class 정보만 담겨있다. // 생성한 객체(obj)를 objPool에 보관하고 리턴한다.
// objPool.put(getBeanName(clazz), obj); // createObject 메서드에 마지막 부분.
// 생성한 obj를 보관할 때, getBeanName메서드를 사용하는데 이 메서드는 밑에서 얘기함.
public Object getBean(String name) {
return objPool.get(name);
}
// objPool에 생성된 모든 객체가 들어있기 때문에, 외부에서 호출 하기 위한 getBean 메서드를 생성한다.
적용 // com.eomcs.util.Component 생성 //
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value() default "";
}
// isComponentClass 코드 나와있는 부분에 뒤에 설명한다는 이야기가 여기를 이야기 하는 것이다.
// *Servlet에 애노테이션 추가 // @Component("이름") // @Component("/board/add") //
// 나중에 이름으로 해당 클래스를 찾을 수 있게 한다. //
private String getBeanName(Class<?> clazz) {
Component compAnno = clazz.getAnnotation(Component.class);
if (compAnno == null || compAnno.value().length() == 0) {
return clazz.getName();
}
return compAnno.value();
}
// createObject에서 만든 객체를 저장할 때, getBeanName의 리턴 값으로 저장한다.
// getBeanName 메서드는 파라미터로 넘어온 Class에 Component 애노테이션이 붙어 있는지 확인하고,
// 붙어 있다면 그 붙어있는 value의 값을 리턴하고, 없다면 해당 class의 이름으로 저장한다.
정리 // 1. ServerApp에서 호출하는 beans는 ApplicationContext의 objPool을 말한다.
// 2. ContextLoaderListener에서 만든 beans는 ApplicationContext에 생성자로 넘겨주지만,
// Context 사용하듯 하나의 바구니에 모든 것을 담아서 움직이는 것이 아닌, 단지 일방통행이라고 생각하면 된다.
// 생성자로 넘겨준 beans는 objPool에 그대로 옮겨 담는다. // objPool안에 Beans의 모든 데이터가 들어감.
// 당연히 Beans는 objPool에 포함되는 관계이기 때문에, beans의 데이터가 훨씬 적다.
// 3. ContextLoaderListener에 Beans는 ApplicationContext에서 자동 생성되지 않는 애들을 담아 두었다.
// 따라서 ApplicationContext objPool에 beans의 데이터를 넘겨주고 담는 이유이다.
// getBean을 보면 objPool에서 get을 꺼내주는 것을 확인 할 수 있다.
// 4. 애노테이션 붙힌 애들이 어떻게 사용되는지 프로세스이다.
// 서버 실행시
// ServerApp에서 context를 ContextLoaderListener에게 건내주며 ServerApp이 쓸 애들을 담아달라고 한다.
// ContextLoaderListener에서 beans를 새로 만들고 자신이 만들어야되는 애들을 만들어 Beans에 담는다.
// ContextLoaderListener가 마지막으로 ApplicationContext를 만들고, 얘가 만들 패키지 목록과 beans를 넘긴다.
// ApplicationContext는 objPool를 새로 만들고 받은 beans의 데이터들을 꺼내 새로 담는다 // 반복문 사용
// 그 후 자신이 만들어야되는 애들을 만들어 저장한다.
// ApplicationContext의 작업이 끝나면, ContextLoaderListener는 Context에 ApplicationContext를 담는다.
// Context안에 ApplicationContext가 objPool을 담고 있는 것.
// 메서드로 호출 할 수 밖에 없어 getBean 메서드를 생성한 것이다.
// Servlet servlet = (Servlet) iocContainer.getBean(request);
// ServerApp에서 Client가 입력한 request를 getBean의 파라미터로 넘긴다.
// 넘긴 파라미터로 servlet class를 찾는다. // servlet.service(in, out); // 해당 class의 service를 호출한다.
// 이런 과정으로 ServerApp이 새로 만들어진 객체들을 사용하는 것이다. // 분석하느라 개힘들었음...
v47 -> 건너뜀.
v48_1 -> 애노테이션으로 메서드 구분 //
// 인터페이스는 규칙이기 때문에 구현이 매우 엄격하다.
// 메서드 이름에서 파라미터 타입/개수, 리턴 타입까지 정확하게 구현해야 한다.
// 애노테이션을 사용하면 인터페이스 보다 더 유현하게 규칙을 처리할 수 있다.
// util.RequestMapping // @interface 애노테이션 추가
// 실행 중에 메서드에 붙은 애노테이션 정보를 추출해야 한다.
@Retention(RetentionPolicy.RUNTIME)
// 메서드에만 이 애노테이션을 붙일 수 있도록 사용 범위를 제한한다.
@Target({ElementType.METHOD})
public @interface RequestMapping {
// 서블릿 메서드와 명령어를 연결하기 위해,
// 명령어를 저장할 프로퍼티를 정의한다.
String value();
}
// XxxServlet 변경 // @Component변경 @RequestMapping("/board/add") 추가
@Component
public class BoardAddServlet {
BoardService boardService;
public BoardAddServlet(BoardService boardService) {
this.boardService = boardService;
}
@RequestMapping("/board/add")
public void service(Scanner in, PrintStream out) throws Exception {
Board board = new Board();
board.setTitle(Prompt.getString(in, out, "제목? "));
boardService.add(board);
out.println("새 게시글을 등록했습니다.");
}
// RequestMapping이라는 전문적으로 애노테이션만 관리하는 애노테이션 추가로 메서드에 이름을 붙힌다.
// util.ApplicationContext 변경
public String[] getBeanNamesForAnnotation(Class<? extends Annotation> annotationType) {
ArrayList<String> beanNames = new ArrayList<>();
Set<String> beanNameSet = objPool.keySet();
for (String beanName : beanNameSet) {
Object obj = objPool.get(beanName);
if (obj.getClass().getAnnotation(annotationType) != null) {
beanNames.add(beanName);
}
}
String[] names = new String[beanNames.size()];
beanNames.toArray(names);
return names;
}
// Annotation을 상속한 클래스를 구현한 Class를 파라미터로 넘겨받는다.
// ArrayList<>() 생성 // 객체 이름을 저장할 목록을 준비한다.
// Set beanNameSet = objPool.keySet(); // 객체풀에서 전체 객체의 이름을 꺼낸다.
// 꺼낸 객체가 파라미터로 지정한 애노테이션이 붙었는지 알아낸다.
// Annotation이 붙은 클래스를 구현한 클래스 이름의 배열을 리턴한다.
RequestMappingHandlerMapping handlerMapper = //
new RequestMappingHandlerMapping();
String[] beanNames = appCtx.getBeanNamesForAnnotation(Component.class);
for (String beanName : beanNames) {
Object component = appCtx.getBean(beanName);
Method method = getRequestHandler(component.getClass());
if (method != null) {
RequestHandler requestHandler = new RequestHandler(method, component);
handlerMapper.addHandler(requestHandler.getPath(), requestHandler);
}
}
// RequestMappingHandlerMapping 생성은 밑에서 얘기한다.
// getBeanNamesForAnnotation을 통해 Component가 붙은 Class 목록을 리턴하고
// 해당 String에서 bean 객체를 꺼내 Object component에 담는다.
// getRequestHandler 메서드에 component의 class 정보를 넘긴다.
private Method getRequestHandler(Class<?> type) {
Method[] methods = type.getMethods();
for (Method m : methods) {
RequestMapping anno = m.getAnnotation(RequestMapping.class);
if (anno != null) {
return m;
}
}
// 파라미터로 넘어온 component class의 메소드 배열을 가져온다.
// RequestMapping anno = m.getAnnotation(RequestMapping.class);
// 해당 메소드에 RequestMapping 애노테이션이 붙었는지 확인하고 붙어있다면, return 한다.
// 없다면 null을 return 한다. // 해당 메소드의 리턴 값이 있다면
// RequestHandler requestHandler = new RequestHandler(method, component);
// 넘어온 메소드 정보와 해당 Class 정보를 RequestHandler로 전달한다.
public class RequestHandler {
Object bean;
String path;
Method method;
public RequestHandler() {
}
public RequestHandler(Method method, Object bean) {
this.method = method;
this.bean = bean;
this.path = getPath(method);
}
private String getPath(Method method) {
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
return requestMapping.value();
}
public Object getBean() {
return bean;
}
public void setBean(Object bean) {
this.bean = bean;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
// 클라이언트 명령을 처리하는 메서드 정보를 준비한다.
// 아직 풀리지 않는 의문 1. 왜 파라미터가 없는 생성자가 존재하는가.
public class RequestMappingHandlerMapping {
HashMap<String, RequestHandler> handlerMap = new HashMap<>();
public void addHandler(String name, RequestHandler requestHandler) {
handlerMap.put(name, requestHandler);
}
public RequestHandler getHandler(String name) {
return handlerMap.get(name);
}
}
// RequestMapping이라는 애노테이션을 설정하여 해당 애노테이션에 맞는 메소드를 호출하게 만들었다.
// 호출 될 때, RequestHandler class에서 해당 Request의 객체를 생성하였다.
// RequestMappingHandlerMapping class를 추가. // 생성된 객체를 보관할, 그리고 생성했던 객체를 받아 사용함.
v49 -> 건너뜀.
v50_1 -> Spring IoC 컨테이너 적용
// mvnmvnrepository.com 또는 search.maven.org 접속 - 'spring context' 검색
// org.springframework:spring-context // 버전 클릭 // Gradle Groovy DSL 복사
// - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'
// com,eomcs.lms.AppConfig 생성 // Spring IoC 컨테이너가 객체를 생성하기 위해 탐색할 패키지
// @ComponentScan(value="com.eomcs.lms") // 애노테이션 추가 // 이 애노테이션은 스프링에서 지원 함
// @ComponentScan // @Component 애노테이션이 붙은 클래스를 찾아 객체를 생성한다.
// com.eomcs.lms.ApplicationContext 삭제 // Spring IoC 컨테이너로 대체
// ContextLoaderListener 변경
// ApplicationContext appCtx = new AnnotationConfigApplicationContext(AppConfig.class);
// import org.springframework.context.ApplicationContext; // import를 교체해주고,
// AnnotationConfigApplicationContext로 생성해주는 것을 바꿔준다.
// 위 생성자는 Spring IoC 컨테이너의 설정 정보를 담고 있는 클래스 타입을 파라미터로 받는다.
// ServerApp 변경 // import를 Spring껄로 바꾸어 준다.
// util.Component 제거 // servlet.* 변경
// @Component 애노테이션 변경 // Spring에서 사용하는 Component를 사용하게끔 변경해야 한다.
// 변경 후 서버를 실행하면 IoC 컨테이너를 생성하는 중에 오류가 발생한다.
// 서비스 객체를 생성하는 중에 의존 객체인 DAO가 없기 때문이다. // DAO를 먼저 준비해야 한다.
@ComponentScan(value = "com.eomcs.lms")
public class AppConfig {
public AppConfig() throws Exception {
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
InputStream inputStream = Resources.getResourceAsStream(//
"com/eomcs/lms/conf/mybatis-config.xml");
return new SqlSessionFactoryProxy(new SqlSessionFactoryBuilder().build(inputStream));
}
@Bean
public MybatisDaoFactory daoFactory(SqlSessionFactory sqlSessionFactory) {
return new MybatisDaoFactory(sqlSessionFactory);
}
@Bean
public PlatformTransactionManager transactionManager(SqlSessionFactory sqlSessionFactory) {
return new PlatformTransactionManager(sqlSessionFactory);
}
@Bean
public BoardDao boardDao(MybatisDaoFactory daoFactory) {
return daoFactory.createDao(BoardDao.class);
}
@Bean
public LessonDao lessonDao(MybatisDaoFactory daoFactory) {
return daoFactory.createDao(LessonDao.class);
}
@Bean
public MemberDao memberDao(MybatisDaoFactory daoFactory) {
return daoFactory.createDao(MemberDao.class);
}
@Bean
public PhotoBoardDao photoBoardDao(MybatisDaoFactory daoFactory) {
return daoFactory.createDao(PhotoBoardDao.class);
}
@Bean
public PhotoFileDao photoFileDao(MybatisDaoFactory daoFactory) {
return daoFactory.createDao(PhotoFileDao.class);
}
// AppConfig 변경 // ContextLoaderListener에서 DAO 객체 생성하는 코드 가져오기
// Spring IoC 컨테이너에 수동으로 객체를 등록하고 싶다면, 펙토리 메서드를 만들어 리턴한다.
// @Bean 애노테이션을 붙혀야 한다 // 그래야 Spring IoC 컨테이너는 이 메서드를 호출하고 리턴 값을 보유한다.
// Spring IoC에서 객체를 저장할 때, 메소드 명으로 저장한다. // 따라서 메서드명이 boardDao 명사형이다.
// daoFactory를 만들 때, sqlSessionFactory가 필요한데, 필요한 값이 있다면 파라미터로 선언만 하면 된다.
// 단, 파라미터로 선언했을 때 필요한 객체가 IoC 컨테이너 안에 있어야만 한다.
// 또한 필요할 때 객체가 생성이 안돼있더라도,
// IoC 컨테이너 안에만 있으면 객체를 생성해서 알아서 파라미터로 넘긴다.
v51_1 ->Spring IoC 컨테이너와 MyBatis 프레임워크 연동
// Mybatis를 Spring IoC 컨테이너와 연결할 때 사용할 의존 라이브러리를 가져온다.
// mvnmvnrepository.com 또는 search.maven.org 접속 - 'mybatis-spring' 검색
// org.mybatis:mybatis-spring // 버전 클릭 // Gradle Groovy DSL 복사
// - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'
// mvnmvnrepository.com 또는 search.maven.org 접속 - 'spring-jbdc' 검색
// org.mybatis:mybatis-spring // 버전 클릭 // Gradle Groovy DSL 복사
// - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'
// AppConfig 변경 //
@PropertySource("classpath:com/eomcs/lms/cof/jdbc.properties")
public class AppConfig {
@Value("${jdbc.driver")
String jdbcDriver;
@Value("${jdbc.url")
String jdbcUrl;
@Value("${jdbc.username")
String jdbcUsername;
@Value("${jdbc.password")
String jdbcPassword;
@Bean
public DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(jdbcDriver);
ds.setUrl(jdbcUrl);
ds.setUsername(jdbcUsername);
ds.setPassword(jdbcUsername);
return ds;
}
// @PropertySource("classpath:com/eomcs/lms/cof/jdbc.properties") // 애노테이션 추가
// 애노테이션을 설정하여 Property를 불러올 수 있다.
// @value("${설정}") 애노테이션을 통해 java 인스턴스로 값을 가져올 수 있다.
// 가져온 값으로 DataSource를 만드는 메서드를 추가한다. // @Bean을 사용하여 객체 생성함.
// PlatformTransactionManager 변경 // SqlSession이 아닌 DataSource를 사용하는 것으로 바꿔준다.
// com.eomcs.sql.TransactionTemplate, TransactionCallback 삭제
// public PlatformTransactionManager transactionManager(DataSource dataSource)
// { return new DataSourceTransactionManager(dataSource); } // Spring에 있는 걸로 교체
// PhotoBoardServiceImpl 변경 // TransactionTemplate, TransactionCallback Spring 용으로 변경
@Override
public void add(PhotoBoard photoBoard) throws Exception {
transactionTemplate.execute(new TransactionCallback<>() {
@Override
public Object doInTransaction(TransactionStatus status) {
try {
if (photoBoardDao.insert(photoBoard) == 0) {
throw new Exception("사진 게시글 등록에 실패했습니다.");
}
photoFileDao.insert(photoBoard);
} catch (Exception e) {
status.setRollbackOnly();
}
return null;
}
});
}
// Spring의 TransactionCallback의 경우 제너릭이 적용되어, <>를 추가해준다.
// 또한 doInTransaction의 파라미터가 있기 때문에 파라미터로 status를 넣어준다 한다.
// Dao를 사용하는 곳에 try문을 사용하여 예외처리를 한다. // Transaction에 throws가 안붙어 있어 처리해야함.
// status.setRollbackOnly를 통해 롤백시키는 예외처리를 추가한다.
// AppConfig 변경 //
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource, ApplicationContext appCtx) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setTypeAliasesPackage("com.eomcs.lms.domain");
sqlSessionFactoryBean.setMapperLocations(appCtx.getResources("classpath:com/eomcs/lms/mapper/*Mapper.xml"));
return sqlSessionFactoryBean.getObject();
}
// SesSqlSessionFactoryBean은 SqlSessionFactory를 만들어주는 빌더이다.
// sqlSessionFactoryBean.setDataSource(dataSource); // mybatios-config의 properties 기능을 대신한다.
// sqlSessionFactoryBean.setTypeAliasesPackage("com.eomcs.lms.domain"); // properties의 typeAlias 기능을 대신
//sqlSessionFactoryBean.setMapperLocations(appCtx.getResources("classpath:com/eomcs/lms/mapper/*Mapper.xml"));
// mybatios-config의 mapper 기능을 대신한다.
// sqlSessionFactoryBean.getObject(); // SesSqlSessionFactoryBean(빌더)에서 Factory를 만들어 달라는 메서드
// com.eomcs.sql.SqlSessionFactoryProxy, SqlSessionProxy삭제
// resource.com.eomcs.lms.conf.mybatis-config.xml 삭제
// AppConfig 변경 // @MapperScan("com.eomcs.lms.dao") 애노테이션 추가 //
// DAO 펙토리 메서드 삭제 // 수동으로 DAO 객체를 만드는 팩토리 메서드를 모두 제거
// com.eomcs.sql.MybatisDaoFactory 삭제 // DAO 만드는 코드를 모두 삭제했고,
// Spring 기능으로 대체하기 때문에 우리가 만든 것은 필요가 없다. // 그래서 삭제
// ServerApp 변경 // SqlSessionFactory 사용 코드 제거
// SessionFactory를 삭제하는 이유는 Spring 프레임 워크에서 트랜잭션을 관리하기 때문이다.
v51_2 -> Spring IoC 설정 파일을 분리하기()// 관리하기 쉽도록 하기 위함
// 현재 AppConfig가 ContextLoaderListener에서 호출 되어, AppConfig 객체를 생성한다.
// com.eomcs.lms.DatabaseConfig 추가 // AppConfig에서 Database 관련한 코드를 옮긴다.
@PropertySource("classpath:com/eomcs/lms/conf/jdbc.properties")
public class DatabaseConfig {
@Value("${jdbc.driver}")
String jdbcDriver;
@Value("${jdbc.url}")
String jdbcUrl;
@Value("${jdbc.username}")
String jdbcUsername;
@Value("${jdbc.password}")
String jdbcPassword;
@Bean
public DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(jdbcDriver);
ds.setUrl(jdbcUrl);
ds.setUsername(jdbcUsername);
ds.setPassword(jdbcPassword);
return ds;
}
@Bean
public PlatformTransactionManager transactionManager(
DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
// com.eomcs.lms.MybatisConfig 추가 // AppConfig에서 Mybatis 관련한 코드를 옮긴다.
@MapperScan("com.eomcs.lms.dao")
public class MybatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource, // DB 커넥션풀
ApplicationContext appCtx // Spring IoC 컨테이너
) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setTypeAliasesPackage("com.eomcs.lms.domain");
sqlSessionFactoryBean.setMapperLocations(//
appCtx.getResources("classpath:com/eomcs/lms/mapper/*Mapper.xml"));
return sqlSessionFactoryBean.getObject();
}
}
// 문제는 실행해도 정상적으로 실행이 안되는데, 에러파일을 보면 Dao를 찾을 수 없어서 발생한 문제로 나온다.
// @Configuration을 붙혀줘야 IoC 컨테이너가 Java Config로 자동 인식하고 확인한다.
// AppConfig의 경우에는 @ComponentScan(value = "com.eomcs.lms") 애노테이션이 붙어있어,
// @Configuration을 붙혀주지 않아도 된다. Component 애노테이션은 지정한 패키지 및 하위 패키지에서
// Component이 붙은 클래스를 알아서 찾아 객체를 생성하는 것이고
// @Configuration는 IoC Container에게 해당 클래스가 Bean 구성 Class이다.라고 알려주는 역할이다.
// 단, 이 클래스가 @ComponentScan 에서 지정한 패키지 안에 있어야 한다.
v52_1 -> 애노테이션을 이용하여 트랜잭션 제어
// PhotoBoardServiceImpl 변경 // 트랜잭션을 애노테이션을 붙혀 처리한다. // 트랜잭션 관련 코드 삭제
// @Transactional //
// DatabaseConfig 변경 // @EnableTransactionManagement 애노테이션 추가
// @EnableTransactionManagement // 메서드에 @Transactional가 붙어 있으면,
// 해당 메서드를 트랜잭션으로 묶기 위해 프록시 객체를 자동 생성한다.
// PhotoBoardServiceImpl 변경 // add(), update(), delete() 변경 // 트랜잭션을 사용하는 메서드
// TransactionTemplate을 사용하는 대신에 @Transactional 애노테이션을 붙인다.
v53_1 -> Log4j 1.2.x를 사용하여 애플리케이션 로그 처리하기
// 기존에 애플리케이션 실행 상태를 확인하고 싶을때 Sysout을 추가하여 값을 출력한다.
// 이 방식의 문제는 개발이 끝난 후 일일히 찾아서 지워줘야 한다는 점이다.
// Log4j란? //
// 로그의 출력형식을 지정할 수 있게 도와주는 라이브러리
// 개발 중에는 로그를 자세하게 출력하고,
// 개발이 완료된 후에는 중요 로그만 출력하도록 조정하는 기능을 제공한다.
// 로그의 출력 형식을 지정할 수 있다. // 출력 대상도 콘솔, 파일, 네트워크, DB 등 다양하게 지정할 수 있다.
// mvnmvnrepository.com 또는 search.maven.org 접속 - 'log4j' 검색
// org.springframework:spring-context // 버전 클릭 // Gradle Groovy DSL 복사
// - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'
// log4j도 두가지 버전이 있다.
// 1. log4j // 1.2.x 버전 // 과거 버전으로 실무에서는 아직도 1.2 버전을 사용하는 경우가 많다.
// 2. log4j // 2.x.x 버전 // 비교적 최신 버전으로, 실무에서 이 경우를 사용하는 경우도 있다.
// 이제부터 XML 따라서 java파일은 .으로 기타파일은 /로 작성하도록 함 // 과거꺼는 무작위로 되어있음.
// src/main/resources/log4j.propertise 추가 // 설정 파일 등록
#log4j.properties
# Set root logger level to DEBUG and its only appender to stdout.
log4j.rootLogger=INFO, stdout
# stdout is set to be a ConsoleAppender.
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
# stdout uses PatternLayout.
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
// Log4j 사용법 // #은 주석을 의미한다.
// 루트 로거(기본 로깅 도구)를 설정 // log4j.rootLogger=로깅레벨, 출력담당자1, 출력담당자2, 출력담당자3 ...
// ex) log4j.rootLogger=ERROR, aaa, bbb, ccc, ...
// 로깅 레벨 // 로깅 레벨은 다음 여섯 개 중 하나를 지정할 수 있다.
// FATAL // 애플리케이션을 중지해야 할 심각한 오류를 의미
// ERROR // 오류가 발생했지만, 애플리케이션을 계속 실행할 수 있는 상태를 의미
// WARN // 잠재적인 위험을 안고 있는 상태를 의미
// INFO // 애플리케이션의 진행 정보를 의미. 프로그램을 실행시키는 시스템 운영자를 위해 출력할 때 주로 사용한다.
// DEBUG // 애플리케이션의 내부 실행 상태를 의미. 프로그래밍 할 때 필요한 정보를 출력할 때 주로 사용한다.
// TRACE // DEBUG 보다 더 자세한 상태를 의미
// 루트 로거에 로깅 레벨을 지정하면 그 하위 로거들에 모두 적용된다.
// 하위 로거에 루트 로거의 레벨 대신 다른 레벨을 지정할 수 있다.
// 출력담당자(Appender) // 출력담당자는 이해를 돕기위한 설명이다. // Appender로 외우자
// 한 개 이상의 Appender를 지정할 수 있다. // Appender 이름으로 출력 방법을 자세하게 정의한다.
// Appender 설정 //
// 출력 방법 지정 // 출력할 도구를 지정한다. 예를 들면, 파일이나 콘솔, 네트워크 등을 지정할 수 있다.
// log4j.appender.출력담당자명=패키지명을 포함한 클래스명
// ex) log4j.appender.aaa=org.apache.log4j.ConsoleAppender // 콘솔 창으로 출력
// ex) log4j.appender.aaa=org.apache.log4j.FileAppender // 파일로 출력
// ex) log4j.appender.aaa=org.apache.log4j.net.SocketAppender //원격 서버로 출력
// 출력 형식 관리자 지정 // 출력할 때의 문자열 형식을 관리할 객체를 설정한다.
// log4j.appender.출력담당자명.layout=패키지명을 포함한 클래스명
// ex) log4j.appender.aaa.layout=org.apache.log4j.SimpleLayout // 한 줄의 간단한 문자열 출력
// ex) log4j.appender.aaa.layout=org.apache.log4j.HTMLLayout // HTML 테이블 형식으로 출력
// ex) log4j.appender.aaa.layout=org.apache.log4j.PatternLayout // 지정된 패턴 명령에 따라 출력
// ex) log4j.appender.aaa.layout=org.apache.log4j.XMLLayout // XML 태그로 출력
// 출력 형식 지정 // 출력 문자열의 형식을 설정한다.
// log4j.appender.출력담당자명.layout.ConversionPattern=패턴 명령
// ex) log4j.appender.aaa.layout.ConversionPattern=%5p [%t] - %m%n
// 패턴 명령 //
// %자릿수p // 등급(예: FATAL/ERROR/WARN/...)을 출력하고 싶을 때 사용한다.
// %t // 스레드 이름(예: main)을 출력하고 싶을 때 사용한다.
// %m // 로그 메시지를 출력하고 싶을 때 사용한다.
// %n // 줄 바꿀 때 사용한다.
// 하위 로거 설정 // 특정 이름의 로거나 특정 자바 패키지(또는 클래스)에 대해 출력 레벨을 설정할 수 있다.
// log4j.logger.로거이름=레벨 // log4j.logger.자바 패키지 이름=레벨
// ex) log4j.logger.okok=DEBUG // log4j.logger.com.eomcs.lms.dao=DEBUG
// ex) log4j.logger.com.eomcs.lms.dao.BoardDao=DEBUG
// ServerApp, ContextLoaderListener, AppConfig, DatabaseConfig, MybatisConfig // Logger 준비.
v53_2 ->log4j 2.x.x 버전 적용 //
// mvnmvnrepository.com 또는 search.maven.org 접속 - 'log4j-core' 검색
// org.springframework:spring-context // 버전 클릭 // Gradle Groovy DSL 복사
// - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'
// src/main/resources/log4j2.xml 추가 //
// ServerApp, ContextLoaderListener, AppConfig, DatabaseConfig, MybatisConfig // 로그 출력을 Log4j2로 전환
// MybatisConfig 변경 // org.apache.ibatis.logging.LogFactory.useLog4J2Logging() 호출
// Mybatis의 log4j2 활성화시키기 위함.
v54_1 -> HTTP 프로토콜 적용
// Web기술 적용
// 통신 : HTTP // UI : HTML // ClientApp : Web Brower // 대체한다.
// method // 서버에 요청하는 명령 // GET, POST, HEAD, PUT, DELETE ... 등 // 일단 5개는 최소 다 알아두자.
// GET // 서버에 자원을 요청하는 명령어.
// POST // 서버에 자원을 보내는 명령어.
// HEAD // 지정한 자원의 정보(문자집합, 생성일 등)만 요구하는 명령어.
// PUT // 지정한 자원의 변경을 요구하는 명령어.
// DELETE // 지정한 자원의 삭제를 요구하는 명령어.
// request-uri // 서버 자원의 경로. // 예) /board/list // 예) /board/list?title=aaa&content=bbb
// http-version // 통신 프로토콜의 버전 // 예) HTTP/1.1
// header // 클라이언트가 서버에 보내는 부가 정보. // 헤더명: '값 CRLF'
// general-header // 요청이나 응답에 모두 사용할 수 있는 부가 정보.
// ex) Connection: close // ex) Date: Tue, 15 Nov 1994 08:12:31 GMT
// request-header // 요청하는 쪽에서 보낼 수 있는 부가 정보
// ex) Accept: text/*, text/html, text/html;level=1, */*
// ex) Accept-Language : ko, en-gb;q=0.8, en;q=0.7
// ex) Referer: Http://www.w3.org/hypertext/DataSources/Overview.html // 어디에서 들어왔는지
// response-header // 응답하는 쪽에서 보낼 수 있는 부가 정보
// ex) Server: CERN/3.0 libwww/2,17
// ex) Location: http://www.w3.org/pub/WWW/People.html
// entity-header // 요청이나 응답에 모두 사용할 수 있는 부가 정보 // 보내는 데이터에 대한 정보
// ex) Content-Type: text/html; charset=UTF-8
// ex) Content-Length:3495
// ex) Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT
// HTTP 요청 예:
// F12 - Network - Headers에서 Request Headers와 Response Header를 확인하자
// status-code
// 1xx // 요청을 받았고, 처리중임을 표현
// 2xx // 성공했음을 표현
// 200 // 요청을 정상적으로 처리했음
// 201 // 요청한 자원을 생성했음
// 3xx // 요청한 자원이 다른 장소로 옮겨졌음을 표현
// 301 // 요청한 자원이 다른 장소로 이동 했음
// 304 // 요청한 자원이 변경되지 않았음. 따라서 기존에 다운로드 받은 것을 그대로 사용하라고 요구함.
// 4xx // 잘못된 형식으로 요청했음을 표현
// 401 요청 권한이 없음.
// 403 // 요청한 자원에 대한 권한이 없어 실행을 거절함.
// 404 // 요청한 자원을 찾을 수 없음.
// 5xx // 서버쪽에서 오류가 발생했음을 표현
// 500 // 서버에서 프로그램을 실행하다가 오류 발생함.
// message-body //
// 클라이언트가 POST로 요청할 때 message-body 데이터를 서버로 보낸다.
// 서버에서 응답할 때 message-body 데이터를 클라이언트로 보낸다.
// HTTP 프로토콜에 따라 데이터를 주고 받는다면, ClientApp대신 Browser를 사용할 수 있다.
// 즉 Web Browser가 ClientApp이 되는 것.
// ServerApp 변경 // HTTP request 처리 코드 추가
String[] requestLine = in.nextLine().split(" ");
String method = requestLine[0];
String requestUri = requestLine[1];
// " "로 어떤 데이터를 나누는 것인가 // 실제 웹에 접속하게 되면,
// GET /board/list HTTP/1.1 과 GET /favicon.ico HTTP/1.1 의 코드가 in.nextLine(0으로 오게 된다.
// 실제 처리에 있어 HTTP/1.1은 중요한게 아니라서 버리고, GET과 /board/list를 가져오는 것이다.
// /favicon.ico // 서버를 대표하는 이미지, Tab키에서 naver로 치면 녹색 n을 말한다.
// HTTP 프로토콜에 맞게 HTTP 응답(response) 헤더 출력하는 메서드 추가
private void printResponseHeader(PrintStream out) {
out.println("HTTP/1.1 200 OK");
out.println("Server: bitcampServer");
out.println();
}
v54_2 -> HTML 형식 적용 // 출력할 때, HTML 형식을 적용한다.
// *Servlet에 HTML 코드를 출력하게끔 변경 //
// BoardListServlet //
@RequestMapping("/board/list")
public void service(Map<String, String> params, PrintStream out) throws Exception {
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head>");
out.println(" <meta charset='UTF-8'>");
out.println(" <title>게시글 목록</title>");
out.println("</head>");
out.println("<body>");
out.println(" <h1>게시글</h1>");
out.println(" <a href='/board/addForm'>새 글</a><br>");
out.println(" <table border='1'>");
out.println(" <tr>");
out.println(" <th>번호</th>");
out.println(" <th>제목</th>");
out.println(" <th>등록일</th>");
out.println(" <th>조회수</th>");
out.println(" </tr>");
List<Board> boards = boardService.list();
for (Board board : boards) {
out.printf(" <tr>"//
+ "<td>%d</td> "//
+ "<td><a href='/board/detail?no=%d'>%s</a></td> "//
+ "<td>%s</td> "//
+ "<td>%d</td>"//
+ "</tr>\n", //
board.getNo(), //
board.getNo(), //
board.getTitle(), //
board.getDate(), //
board.getViewCount() //
);
}
out.println("</table>");
out.println("</body>");
out.println("</html>");
}
// out.println으로 ClientApp인 Web Browser에게 HTML 코드를 전달한다.
// HTML은 추후 더 자세히 배울 예정
// <a href='/board/addForm'>새 글<br> // 새 글에 /board/addForm 링크를 건다.
// AddFormServelt 추가// /board/addForm을 처리할 servlet을 생성한다.
// AddForm에서 입력을 받고 Add로 데이터를 넘겨 DB에 저장하는 역할을 담당하게 한다.
// Dao에서 default 메서드를 추상메서드로 바꾼다.
// default로 되어 있으면 IoC 컨테이너의 Dao객체가 메서드를 호출하지 않는다.
String servletPath = getServletPath(requestUri);
Map<String, String> params = getParameters(requestUri);
// requestUri // ex) /member/add?email=aaa@test.com&name=aaa&password=1111
// requestUri가 아까 맨 위처럼 /board/list로 올 수도 있지만, 뒤에 값을 달고 올 수도 있다.
private String getServletPath(String requestUri) {
return requestUri.split("\\?")[0];
// getServletPath() 메서드 추가 // 어떤 작업을 요청하는지 알기 위해 메서드를 추가한다.
// ?를 기준으로 앞을 추출한다. // /member/add
private Map<String, String> getParameters(String requestUri) throws Exception {
Map<String, String> params = new HashMap<>();
String[] items = requestUri.split("\\?");
if (items.length > 1) {
String[] entries = items[1].split("&");
for (String entry : entries) {
String[] kv = entry.split("=");
if (kv.length > 1) {
String value = URLDecoder.decode(kv[1], "UTF-8");
params.put(kv[0], value);
} else {
params.put(kv[0], "");
}
}
}
return params;
}
// getParameters() 메서드 추가 //넘겨받은 requestUri에서 파라미터 값을 추출할 메서드를 추가한다.
// ?를 기준으로 뒤를 추출하고 item에 담는다. // email=aaa@test.com&name=aaa&password=1111
// 담은 item을 & 기준으로 분할한다. // email=aaa@test.com // name=aaa // password=1111
// 담은 entries들을 = 기준으로 분할한다. // = 뒤의 값이 있으면 앞의 값과 묶어 Map 객체에 담는다
// 목적은 넘겨받은 Uri에서 파라미터 값을 추출하기 위함이다.
// 앞의 값은 파라미터 항목이 되고, 뒤의 값은 그 항목에 대한 값인 것이다.
// 그러나 넘겨받은 Uri 데이터는 웹 브라우저가 인코딩하여 보낸데이터이다.
// 따라서 decode를 통해 String 변수에 담아 보관한다.
// 나눈 entries들의 작업이 다 끝났으면, params를 리턴한다.
// 이 파라미터의 값은 추후 requestHandler를 호출하고, 해당 requestHandler가 invoke를 실행할 때 넘겨준다.
v54_3 -> Apache HttpComponents 라이브러리 사용 웹 서버 구현
// mvnmvnrepository.com 또는 search.maven.org 접속 - 'httpclient5' 검색
// org.springframework:spring-context // 버전 클릭 // Gradle Groovy DSL 복사
// - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'
// 등록한 라이브러리를 이용, HTTP 서버를 작동한다.
SocketConfig socketConfig = SocketConfig.custom()
.setSoTimeout(15, TimeUnit.SECONDS)
.setTcpNoDelay(true)
.build();
HttpServer server = ServerBootstrap.bootstrap()
.setListenerPort(9999)
.setSocketConfig(socketConfig)
.setSslContext(null)
.setExceptionListener(this)
.register("*", this)
.create();
server.start();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
notifyApplicationDestroyed();
logger.info("서버 종료!");
server.close(CloseMode.GRACEFUL);
}
});
// SocketConfig socketConfig = SocketConfig.custom() // 소캣 동작을 설정한다.
// .setSoTimeout(15, TimeUnit.SECONDS) //
// .setTcpNoDelay(true).build(); //
// HttpServer server = ServerBootstrap.bootstrap()
// .setListenerPort(9999) // 웹서버 포트 번호 설정
// .setSocketConfig(socketConfig) // 기본 소켓 동작 설정
// .setSslContext(null) // SSL 설정
// .setExceptionListener(this) // 예외 처리자 설정
// .register("*", this) // 요청 처리자 설정
// .create(); // 웹서버 객체 생성
// Runtime.getRuntime().addShutdownHook(new Thread() // 웹 서버를 종료시키는 스레드를 등록한다.
// server.awaitTermination(TimeValue.MAX_VALUE); // 이전에 모든 쓰레드 끝날때까지 기다리던 코드라고 생각.
'IT Developer > Bitcamp' 카테고리의 다른 글
비트캠프 프론트엔드 및 백엔드 개발자 #Project v55~60 (0) | 2020.03.31 |
---|---|
비트캠프 프론트엔드 및 벡엔드 개발자 DB (0) | 2020.03.26 |
비트캠프 프론트엔드 및 벡엔드 개발자 net, netty, reflect (0) | 2020.03.11 |
비트캠프 프론트엔드 및 벡엔드 개발자 ioc, jdbc, mybatis, (0) | 2020.03.11 |
비트캠프 프론트엔드 및 백엔드 개발자 SQL (0) | 2020.02.18 |
비트캠프 프론트엔드 및 백엔드 개발자 #Project v26 ~ v42 (0) | 2020.01.21 |
비트캠프 프론트엔드 및 백엔드 개발자 #Project v1 ~ v25 (0) | 2020.01.13 |
비트캠프 프론트엔드 및 벡엔드 개발자 annotation, concurrent, corelib, exception, generic, httpcomponents, io (0) | 2020.01.06 |