반응형

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 &lt;= #{endDate}</if>
      <if test="totalHours != null">and tot_hr &lt;= #{totalHours}</if>
      <if test="dayHours != null">and day_hr &lt;= #{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); // 이전에 모든 쓰레드 끝날때까지 기다리던 코드라고 생각.

반응형

+ Recent posts