[231012] <11> Mybatis, mapper, 통합테스트, 스케줄러(cron), 인터셉터

MJ·2023년 10월 22일

수업 TIL🐣💚

목록 보기
61/68
post-thumbnail

1교시

위 코드는 AppConfig에 application.properties 연결한 것

# HikariCP
spring.datasource.hikari.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.hikari.jdbc-url=jdbc:log4jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.hikari.username=GD
spring.datasource.hikari.password=1111

# mybatis
mybatis.config-location=mybatis/config/mybatis-config.xml
mybatis.mapper-locations=mybatis/mapper/*.xml

어제 DataSource를 히카리로 대체하고 히카리에서 사용하는 언어 기반으로 application.properties(src/main/resources, 위 코드)에 db 정보를 넣어놓고 불러들이는 코드를 제작해둔 상태


  1. AppConfig의 DataSource를 히카리로 바꾼다.
  • sqlSessionFactory와 sqlSessionTemplate를 작성하려면 mybatis-config.xml과 mapper.xml 필요 - jspstudy 9장에서 mybatis 폴더 통째로 가져오기
    • mybatis-config.xml : mybatis 환경설정을 여기서 했었다.
    • mapper.xml : 쿼리문 환경설정을 여기서 했었다. (book -> contact 이름 변경)

2교시 mapper

쿼리문을 옮겨서 mapper를 완성하는 작업
contactDao -> src/main/resources/mybatis/mapper/contact.xml


이런식으로 (아래꺼를 위로) 다 옮기고 나중에 mybatis에 맞게 db 부분 수정할 거임. insert update delete select 다 옮긴다.

기본 설명

  • id: 이 태그를 호출할 메소드의 이름과 맞춤 (사진에서 id는 insert가 된다)
  • parameterType: 메소드에 전달되는 데이터, 패키지도 적어줘야 한다.
  • resultType="int"를 원래 적어줘야하지만 니가 안적어도 int 반환해야하는거 알고있으니까 그냥 적지마라. insert 태그에서는 반환값을 생략한다. (resultType 속성은 <select> 엘리먼트에서 생략될 수 없는 속성. 나머지는 생략 가능)
  • ? -> #{필드이름} 형태로 교체

select

  • 파라미터 없으므로 parameterType 생략
  • 결과가 리스트 형태인 List<ContactDto> -> 결과타입이 있으므로 resultType을 적는다. 주의. ContactDto가 리스트에 들어가있어도 리스트라고 하지 않고 리스트에 실제로 들어가 있는 타입 즉 ContactDto만 적으면 된다. (collections의 경우, collection에 포함된 type 작성)

contact.xml 완성본

원래는 파라미터 타입이랑 리턴타입 전부 패키지 이름 일일이 써서 parameterType="com.gdu.app11.dto.ContactDto" 이런 식으로 써줬던건데 3교시에 배울 별명 지정으로 간단화된 코드임

<mapper namespace="mybatis.mapper.contact">

  <insert id="insert" parameterType="ContactDto">
  INSERT INTO CONTACT_T(CONTACT_NO, NAME, TEL, EMAIL, ADDRESS, CREATED_AT) VALUES(CONTACT_SEQ.NEXTVAL, #{name}, #{tel}, #{email}, #{address}, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'))
  </insert>
  
  <update id="update" parameterType="ContactDto">
  UPDATE CONTACT_T SET NAME = #{name}, TEL = #{tel}, EMAIL = #{email}, ADDRESS = #{address} WHERE CONTACT_NO = #{contactNo}
  </update>
  
  <delete id="delete" parameterType="int">
  DELETE FROM CONTACT_T WHERE CONTACT_NO = #{contactNo}
  </delete>
  
  <select id="selectList" resultType="ContactDto">
  SELECT CONTACT_NO, NAME, TEL, EMAIL, ADDRESS, CREATED_AT FROM CONTACT_T ORDER BY CONTACT_NO ASC
  </select>
  
  <select id="selectContactByNo" parameterType="int" resultType="ContactDto">
  SELECT CONTACT_NO, NAME, TEL, EMAIL, ADDRESS, CREATED_AT FROM CONTACT_T WHERE CONTACT_NO = #{contactNo}
  </select>
  
</mapper>

contactDao 원래버전

@Repository
public class ContactDao {

  @Autowired  // Spring Container에 저장된 JdbcConnection 타입의 객체(Bean)을 가져온다.
  private JdbcConnection jdbcConnection;
  
  private Connection con;
  private PreparedStatement ps;
  private ResultSet rs;
  
  /**
   * 삽입 메소드<br>
   * @param contactDto 삽입할 연락처 정보(name, tel, email, address)
   * @return insertCount 삽입된 행(Row)의 개수, 1이면 삽입 성공, 0이면 삽입 실패
   */
  public int insert(ContactDto contactDto) {
    
    int insertCount = 0;
    
    try {
      con = jdbcConnection.getConnection();
      String sql = "INSERT INTO CONTACT_T(CONTACT_NO, NAME, TEL, EMAIL, ADDRESS, CREATED_AT) VALUES(CONTACT_SEQ.NEXTVAL, ?, ?, ?, ?, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'))";
      ps = con.prepareStatement(sql);
      ps.setString(1, contactDto.getName());
      ps.setString(2, contactDto.getTel());
      ps.setString(3, contactDto.getEmail());
      ps.setString(4, contactDto.getAddress());
      insertCount = ps.executeUpdate();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      jdbcConnection.close(con, ps, rs);
    }
    return insertCount;
  }
  
  /**
   * 수정 메소드<br>
   * @param contactDto 수정할 연락처 정보(contact_no, name, tel, email, address)
   * @return updateCount 수정된 행(Row)의 개수, 1이면 수정 성공, 0이면 수정 실패
   */
  public int update(ContactDto contactDto) {
        
    int updateCount = 0;
    
    try {
      con = jdbcConnection.getConnection();
      String sql = "UPDATE CONTACT_T SET NAME = ?, TEL = ?, EMAIL = ?, ADDRESS = ? WHERE CONTACT_NO = ?";    
      ps = con.prepareStatement(sql);
      ps.setString(1, contactDto.getName());
      ps.setString(2, contactDto.getTel());
      ps.setString(3, contactDto.getEmail());
      ps.setString(4, contactDto.getAddress());
      ps.setInt(5, contactDto.getContact_no());
      updateCount = ps.executeUpdate();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      jdbcConnection.close(con, ps, rs);
    }
    return updateCount;
  }
  
  /**
   * 삭제 메소드<br>
   * @param contact_no 삭제할 연락처 번호
   * @return deleteCount 삭제된 행(Row)의 개수, 1이면 삭제 성공, 0이면 삭제 실패
   */
  public int delete(int contact_no) {
    
    int deleteCount = 0;
    
    try {
      con = jdbcConnection.getConnection();
      String sql = "DELETE FROM CONTACT_T WHERE CONTACT_NO = ?";
      ps = con.prepareStatement(sql);
      ps.setInt(1, contact_no);
      deleteCount = ps.executeUpdate();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      jdbcConnection.close(con, ps, rs);
    }
    return deleteCount;
  }
  
  /**
   * 전체 조회 메소드<br>
   * @return 조회된 모든 연락처 정보(ContactDto)
   */
  public List<ContactDto> selectList() {
    
    List<ContactDto> list = new ArrayList<ContactDto>();
    
    try {
      con = jdbcConnection.getConnection();
      String sql = "SELECT CONTACT_NO, NAME, TEL, EMAIL, ADDRESS, CREATED_AT FROM CONTACT_T ORDER BY CONTACT_NO ASC";
      ps = con.prepareStatement(sql);
      rs = ps.executeQuery();
      while(rs.next()) {
        ContactDto contactDto = new ContactDto();
        contactDto.setContact_no(rs.getInt("CONTACT_NO"));
        contactDto.setName(rs.getString("NAME"));
        contactDto.setTel(rs.getString("TEL"));
        contactDto.setEmail(rs.getString("EMAIL"));
        contactDto.setAddress(rs.getString("ADDRESS"));
        contactDto.setCreated_at(rs.getString("CREATED_AT"));
        list.add(contactDto);
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      jdbcConnection.close(con, ps, rs);
    }
    return list;
  }
  
  /**
   * 상세 조회 메소드<br>
   * @param contact_no 조회할 연락처 번호
   * @return contactDto 조회된 연락처 정보, 조회된 연락처가 없으면 null 반환
   */
  public ContactDto selectContactByNo(int contact_no) {
    
    ContactDto contactDto = null;
    
    try {
      con = jdbcConnection.getConnection();
      String sql = "SELECT CONTACT_NO, NAME, TEL, EMAIL, ADDRESS, CREATED_AT FROM CONTACT_T WHERE CONTACT_NO = ?";
      ps = con.prepareStatement(sql);
      ps.setInt(1, contact_no);
      rs = ps.executeQuery();
      if(rs.next()) {
        contactDto = new ContactDto();
        contactDto.setContact_no(rs.getInt(1));
        contactDto.setName(rs.getString(2));
        contactDto.setTel(rs.getString(3));
        contactDto.setEmail(rs.getString(4));
        contactDto.setAddress(rs.getString(5));
        contactDto.setCreated_at(rs.getString(6));
      }
      
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      jdbcConnection.close(con, ps, rs);
    }
    return contactDto;
  }
}

contactDao 수정된 버전

@RequiredArgsConstructor
@Repository
public class ContactDao {

  private final SqlSessionTemplate sqlSessionTemplate;
  
  /**
   * 삽입 메소드<br>
   * @param contactDto 삽입할 연락처 정보(name, tel, email, address)
   * @return insertCount 삽입된 행(Row)의 개수, 1이면 삽입 성공, 0이면 삽입 실패
   */
  public int insert(ContactDto contactDto) {
    return sqlSessionTemplate.insert("mybatis.mapper.contact.insert", contactDto);
  }
  
  /**
   * 수정 메소드<br>
   * @param contactDto 수정할 연락처 정보(contact_no, name, tel, email, address)
   * @return updateCount 수정된 행(Row)의 개수, 1이면 수정 성공, 0이면 수정 실패
   */
  public int update(ContactDto contactDto) {
    return sqlSessionTemplate.update("mybatis.mapper.contact.update", contactDto);
  }
  
  /**
   * 삭제 메소드<br>
   * @param contactNo 삭제할 연락처 번호
   * @return deleteCount 삭제된 행(Row)의 개수, 1이면 삭제 성공, 0이면 삭제 실패
   */
  public int delete(int contactNo) {
    return sqlSessionTemplate.delete("mybatis.mapper.contact.delete", contactNo);
  }
  
  /**
   * 전체 조회 메소드<br>
   * @return 조회된 모든 연락처 정보(ContactDto)
   */
  public List<ContactDto> selectList() {
    return sqlSessionTemplate.selectList("mybatis.mapper.contact.selectList");
  }
  
  /**
   * 상세 조회 메소드<br>
   * @param contactNo 조회할 연락처 번호
   * @return contactDto 조회된 연락처 정보, 조회된 연락처가 없으면 null 반환
   */
  public ContactDto selectContactByNo(int contactNo) {
    return sqlSessionTemplate.selectOne("mybatis.mapper.contact.selectContactByNo", contactNo);
  }
}

왼쪽 mybatis 전, 오른쪽 mybatis 후
엄청나게 간단해졌다.

설명

  • 선언하는 필드가 (bean으로 만들어준 상태니까) @RequiredArgsConstructor 에 의해서 자동으로 Autowired됨
  • 메소드 내용은 사실상 전부 대체돼서 수행할게 없어지고 sqlSessionTemplate의 각각 insert, update, delete, selectList, selectOne 메소드 호출
  • 인수 설명. 첫번째: 호출할 쿼리문의 namespace.id, 두번째: 전달할 파라미터. id를 적을 때 namespace도 적어주는 이유=mapper는 테이블마다 하나씩 나오기 때문에 위치를 지정해줘야 함
  • 셀렉트를 돌리는 쿼리는 selectList와 selectOne로 구분할 수 있는데 selectList()는 목록조회 개념이기 때문에 selectList. 전달할 파라미터가 없으므로 (맨처음부터 이 select는 파라미터 전달 없었다) id만 적고
    selectContactByNo()는 결과가 하나니까 selectOne
  • 다 됐음. serviceImpl, controller, jsp파일 가서 변수 이름 수정
    (contact_no -> contactNo)

3교시

mapper에서 별칭 지정

mapper에서 사용할 수 있는 유용한 옵션

파라미터타입이랑 리턴타입에 패키지 이름까지 구구절절 적어줘야하는데 너무 김.
예시 parameterType="com.gdu.app11.dto.ContactDto"
이걸 별명 지정해서 간단하게 적을 수 있음

  1. mybatis-config.xml 연다
  2. 다음 코드 추가
  <typeAliases>
    <typeAlias alias="ContactDto" type="com.gdu.app11.dto.ContactDto"/>
  </typeAliases>

alias: 돌려서 부를 별명
type: 원래 패키지명

로그 info 레벨로 바꾸기


로그찍어봤더니 hikari(사진)랑 mybatis, ibatis가 debug레벨까지 다찍혀서 보기 힘들다. 안찍히도록 info레벨로 바꾸자

  1. logback.xml
  2. 아래같이 적는다.
  <logger name="com.zaxxer.hikari"   level="info" />
  <logger name="org.mybatis"         level="info" />
  <logger name="org.apache.ibatis"   level="info" />

@Transactional 이용한 트랜잭션 처리

저번에 했던 것 : AoP를 이용한 처리

주소: ContactController에 남아있다.

tx.do에 매핑되면 txText()가 돌아서 하나는 insert 성공, 하나는 insert 실패가 되어아 한다.
(트랜잭션 처리를 안하면 성공은 들어가지만 실패는 들어가지 않는다)
하나의 서비스를 구성하는 모든 DB작업은 전부 성공하거나 전부 실패해야 한다. -> 트랜잭션의 원자성

방법: ContactServiceImpl에 @Transactional 추가하고 AppConfig에서 @Transactional 허용한다는 뜻으로 @EnableTransactionManagement 추가 (aop에서는 @EnableAspectJAutoProxy에 의해서 돌았음)

@Transactional
@RequiredArgsConstructor  // private final ContactDao contactDao;에 @Autowired를 하기 위한 코드이다.
@Service  // ContactService 타입의 객체(Bean)을 Spring Container에 저장한다.
public class ContactServiceImpl implements ContactService {

  private final ContactDao contactDao;
  
  @Override
  public int addContact(ContactDto contactDto) {
    int addResult = contactDao.insert(contactDto);
    return addResult;
  }

  @Override
  public int modifyContact(ContactDto contactDto) {
    int modifyResult = contactDao.update(contactDto);
    return modifyResult;
  }

  @Override
  public int deleteContact(int contactNo) {
    int deleteResult = contactDao.delete(contactNo);
    return deleteResult;
  }

  @Transactional(readOnly=true)  // 조회용(성능 이점)
  @Override
  public List<ContactDto> getContactList() {
    return contactDao.selectList();
  }

  @Transactional(readOnly=true)  // 조회용(성능 이점)
  @Override
  public ContactDto getContactByNo(int contactNo) {
    return contactDao.selectContactByNo(contactNo);
  }
  
  @Override
  public void txTest() {
    
    // @Transactional 활용한 트랜잭션 처리 테스트 메소드
    
    // "성공1개+실패1개" DB처리를 동시에 수행했을 때 모두 실패로 되는지 확인하기
    
    // 성공
    contactDao.insert(new ContactDto(0, "이름", "전화번호", "이메일", "주소", null));
    
    // 실패
    contactDao.insert(new ContactDto());  // NAME 칼럼은 NOT NULL이므로 전달된 이름이 없으면 Exception이 발생한다.
    
  }
}
  • 트랜잭션 처리 안해줘도 되는 애들(데이터 수정이 없는 애들)은 @Transactional(readOnly=true) 를 명시해주는게 좋다. 안해줘도 되는데. 어차피 트랜잭션 안하는 애들이니까 처리할 때 빼주는게 성능에 좋음

12장도 mybatis 한번 더 한다. dao를 더 짧게 할 수 있다.

12장에서 소개하는 걸로 최종 파이널까지 감.

aop 복습 (대충 적어야지 ㅠㅠ 아직 잘 모름)

LogAoP

@Slf4j
@Aspect
@Component
public class LogAop {

  // 포인트컷 : 어떤 메소드에서 동작하는지 표현식으로 작성
  @Pointcut("execution(* com.gdu.app11.controller.*Controller.*(..))")
  public void setPointCut() {}
  
  // 어드바이스 : 포인트컷에서 실제로 동작할 내용
  @Before("setPointCut()")
  public void doLog(JoinPoint joinPoint) {  // JoinPoint : 어드바이스로 전달되는 메소드
    
    // 1. HttpServletRequest
    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = servletRequestAttributes.getRequest();
    
    // 2. 요청 파라미터 -> Map 변환
    Map<String, String[]> map = request.getParameterMap();
    
    // 3. 요청 파라미터 출력 형태 만들기
    String params = "";
    if(map.isEmpty()) {
      params += "No Parameter";
    } else {
      for(Map.Entry<String, String[]> entry : map.entrySet()) {
        params += entry.getKey() + ":" + Arrays.toString(entry.getValue()) + " ";
      }
    }
    
    // 4. 로그 찍기 (치환 문자 {} 활용)
    log.info("{} {}", request.getMethod(), request.getRequestURI());  // 요청 방식, 요청 주소
    log.info("{}", params);                                           // 요청 파라미터
  }
}
  • 어드바이스에 @Before를 해줬으니 포인트컷에 등록된 setPointCut() 함수가 동작하기 전에 어드바이스 들어감
  • 어드바이스 종류에 따라 만드는 방법 정해져있었음. before는 반환타입 : void, 메소드명 : 마음대로, 매개변수 : JoinPoint
  • JoinPoint: 포인트컷의 대상이 될 수 있는 전체 메소드 (어드바이스로 전달되는 메소드) 조인포인트가 전체이고 그 중 일부가 포인트컷
  • ex#%@#가 setPointCut()으로 오면 취소선 그어놓은 것들은 필요가 없다고 함. 그렇게 before만으로도 처리할 수 있다고 함,,,, (무슨말이지?)
  • 로그 코드는 어제 했던 거 복사해서 doLog(어드바이스)안에 붙여넣기 함
  • 로그 코드 대충 설명: request를 구한다음에 request의 파라미터를 전부 빼서 map으로 바꾸고, for문으로 map 순회하면서 파라미터 이름(entry.getKey())이랑 파라미터 값(entry.getValue()) 출력.

4교시

통합테스트

(다 ContactControllerTest)

어노테이션

// JUnin4를 사용한다.
@RunWith(SpringJUnit4ClassRunner.class)
// 테스트에서 사용할 Bean이 @Component로 생성되었다.
@ContextConfiguration(locations="file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml")
// 메소드 이름 순으로 테스트를 수행한다.
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
// WebApplicationContext를 사용할 수 있다. (Spring Container에 Bean을 생성해 둔다.)
@WebAppConfiguration
// 로그
@Slf4j
  • servlet-context.xml에 component-scan으로 인해 컴포넌트로 인식되기 때문에 contextConfiguration에서 servlet-context 파일의 경로를 잡아줘야함. 컴포넌트 찾아볼 위치를 지정하는 것(src부터 다 적어서 servlet-context.xml 경로 지정해주면 된다)
    -픽스메소드오더: 테스트 수행 순서, methodSorters.NAME_ASCENDING = 메소드의 이름순

  • 여기까지가 junit 테스트에서도 사용했던 것.
    junit 단위 테스트에서는 톰캣이 사용되지 않았었다. dao만 있으면 테스트 가능

  • 통합 테스트는 컨트롤러로 테스트하기 때문에 톰캣의 개입이 필요
    => @WebAppConfiguration 적기 -> webApplicationContext를 Bean으로 생성한다

로그찍기 수행

  • autowired로 webApplicationContext를 가져온다.
  • webApplicationtext : 우리가 실제로 필요한 건 MockMvc라는 객체이고 MockMvc를 가지고 테스트를 수행할건데 MockMvc는 webApplicationtext를 필요로 함. (MockMvc는 webApplicationtext를 기반으로 만들어졌기 때문에)
public class ContactControllerTest {
  // MockMvc 객체가 생성될 때 필요한 WebApplicationContext
  @Autowired
  private WebApplicationContext webApplicationContext;
  
  // MockMvc 객체 : 테스트를 수행하는 객체
  private MockMvc mockMvc;
  
  // 테스트 수행 이전에 MockMvc mockMvc 객체를 만든다.
  @Before
  public void setUp() throws Exception {
    mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
  }
  // 삽입테스트
  // 요청 방식 : POST
  // 요청 주소 : /contact/add.do
  // 요청 파라미터 : name, tel, email, address
  @Test
  public void test01_삽입() throws Exception {
    log.info(mockMvc
              .perform(MockMvcRequestBuilders                  // 요청 수행
                        .post("/contact/add.do")               // 요청 방식, 주소
                        .param("name", "뽀로로")               // 요청 파라미터
                        .param("tel", "02-111-1111")
                        .param("email", "pororo@naver.com")
                        .param("address", "pororo villiage"))
              .andReturn()                                     // 요청 결과
              .getFlashMap()                                   // 요청 결과가 저장된 flash attribute를 Map으로 가져옴
              .toString());
  }
  // 상세조회테스트
  // 요청 방식 : GET
  // 요청 주소 : /contact/detail.do
  // 요청 파라미터 : contactNo
  @Test
  public void test02_상세조회() throws Exception {
    log.info(mockMvc
              .perform(MockMvcRequestBuilders
                        .get("/contact/detail.do")
                        .param("contactNo", "1"))
              .andReturn()
              .getModelAndView()
              .getModelMap()
              .toString());
  }
}
  • perform에는 요청을 깔아주고 오청 끝난 후 param에 파라미터들 넣은 후 .andReturn()
  • 컨트롤러에서 add 실행결과는 addResult에 저장되게 해줬었다. 또 그 결과는 FlashAttribute로 저장된다. add 결과 -> FlashAttribute에 저장

    따라서 수행 결과를 확인할 때 .getFlashMap()을 해준다 (FlashAttribute로 저장된 결과들을 map으로 가져오라)


상세조회는 flush 어쩌구 아니고 그냥 model에 저장한 attribute라 getModelAndView로 가져오고 (모델 생기기 전에는 모델앤뷰라는 클래스가 있었다.) 보기 불편하니까 겟모델뷰로 모델로 바꿔서 투스트링으로 문자열로 변환


디벨로퍼 열어서 db 생성해주고

테스트

결과

콘솔에서도 삽입과 조회 로그 확인 가능


5교시

Mybatis를 인터페이스로 쓰기 (12장으로 최종버전 삼으면 됨)
dao는 본문이 있어서 메소드로 본문이 가능한데 인터페이스는 불가능

인터페이스를 쓰는 두가지 방식
1. mapper를 아예 안쓰는 방식 - 쿼리문을 애노테이션화 (쿼리문을 문자열 형식으로 자바에서 작성해야해서 불편함)
2. mapper와 인터페이스를 연결하는 방식 (더 편함. 이걸로 할거)

ContactMapper.java 인터페이스

@Mapper

/*
 * @Mapper
 * 1. 매퍼의 쿼리문을 호출할 수 있는 인터페이스에 추가하는 마이바티스 어노테이션이다.
 * 2. 메소드이름과 호출할 쿼리문의 아이디(id)을 동일하게 맞추면 자동으로 호출된다.
 * 3. @Mapper로 등록된 인터페이스의 검색이 가능하도록 @MapperScan을 추가해야 한다. (SqlSessionTemplate Bean이 등록된 AppConfig.java에 추가한다.)
 * 4. 매퍼의 namespace값을 인터페이스 경로로 작성한다. (원래는 매퍼의 위치로 함)
 */

@Mapper
public interface ContactMapper {
  public int insert(ContactDto contactDto);
  public int update(ContactDto contactDto);
  public int delete(int contactNo);
  public List<ContactDto> selectList();
  public ContactDto selectContactByNo(int contactNo);
  public int deleteOldestContact();
}

Appconfig

@MapperScan(basePackages="com.gdu.app12.dao")

추가

contactMapper.xml (매퍼있는 곳)

<mapper namespace="com.gdu.app12.dao.ContactMapper">

  <insert id="insert" parameterType="ContactDto">
  INSERT INTO CONTACT_T(CONTACT_NO, NAME, TEL, EMAIL, ADDRESS, CREATED_AT) VALUES(CONTACT_SEQ.NEXTVAL, #{name}, #{tel}, #{email}, #{address}, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'))
  </insert>
  
  <update id="update" parameterType="ContactDto">
  UPDATE CONTACT_T SET NAME = #{name}, TEL = #{tel}, EMAIL = #{email}, ADDRESS = #{address} WHERE CONTACT_NO = #{contactNo}
  </update>
  
  <delete id="delete" parameterType="int">
  DELETE FROM CONTACT_T WHERE CONTACT_NO = #{contactNo}
  </delete>
  
  <select id="selectList" resultType="ContactDto">
  SELECT CONTACT_NO, NAME, TEL, EMAIL, ADDRESS, CREATED_AT FROM CONTACT_T ORDER BY CONTACT_NO ASC
  </select>
  
  <select id="selectContactByNo" parameterType="int" resultType="ContactDto">
  SELECT CONTACT_NO, NAME, TEL, EMAIL, ADDRESS, CREATED_AT FROM CONTACT_T WHERE CONTACT_NO = #{contactNo}
  </select>
  
  <delete id="deleteOldestContact">
    DELETE
      FROM CONTACT_T
     WHERE CONTACT_NO = (SELECT MIN(CONTACT_NO)
                           FROM CONTACT_T)
  </delete>
  
</mapper>

사실상 컨택매퍼 인터페이스랑 컨택매퍼 xml은 하나인데 xml에 있는 걸 직접 부를수는 없으니 인터페이스를 사용하는 것. 인터페이스를 부르면 똑같은 이름을 가진 아이디를 찾아서 호출


이제 끝낫다 ㅎㅎ


스케줄러 (cron 작성법)

  • 시간을 걸어주고 자동으로 동작하게 하는 것 (일반적으로 새벽에 돌아가는 걸 원할 때 많이 사용)
  • cron식을 이용해서 타이밍 잡아주는 게 가장 대중적인 방법

6교시

cron

  1. 특정 시간이 특정 서비스를 동작시킬 수 있는 프로그램이다.
  2. 크론식으로 동작할 시간을 지정할 수 있다.
  3. 크론식
    1) 형식
    초 분 시 일 월 요일
    2) 상세
    (1) 초 : 0 ~ 59
    (2) 분 : 0 ~ 59
    (3) 시 : 0 ~ 23
    (4) 일 : 1 ~ 31
    (5) 월 : 0 ~ 11 (JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC)
    (6) 요일 : 1 ~ 7 (MON, TUE, WED, THR, FRI, SAT, SUN)
    3) 방식
    (1) ? : 설정 없음 ("요일"에서 사용)
    (2) : 매번 ("초 분 시 일 월"에서 사용)
    (3) - : 범위
    (4) / : 주기
    4) 예시
    (1) 특정 시간 (금요일 19시 0분 0초에 동작)
    0 0 19
    FRI
    (2) 특정 범위 (새벽 1시 0분 0초부터 매분마다(0-30) 동작)
    0 0-30 1
    ?
    (3) 반복 (10초마다 동작)
    0/10
    * * ?
    (4) 반복 (10분마다 동작)
    0 0/10
    * ?
    (5) 반복 (7시, 19시마다 동작)
    0 0 7,19
    * ?
  4. 크론식 만들기
    https://www.cronmaker.com/

AppConfig

@EnableScheduling             // @Scheduled 허용

우리가 만든 클래스는 bean이 아니라 component로 등록
(원래 AppConfig에 아래 코드 적고 ContactScheduler 가서 실행해줬는데 아래 코드 지우고 ContactScheduler에 @Component 붙이는 걸로 바꿈

5초마다 반복되는 크론식

ContactScheduler


1분마다 가장 예전에 등록된 연락처를 삭제하는 스케쥴러

contactMapper.xml

  <delete id="deleteOldestContact">
    DELETE
      FROM CONTACT_T
     WHERE CONTACT_NO = (SELECT MIN(CONTACT_NO)
                           FROM CONTACT_T)
  </delete>

ContactScheduler

@RequiredArgsConstructor
@Slf4j
@Component
public class ContactScheduler {

  private final ContactService contactService;
  
  @Scheduled(cron="0 0/1 * * * ?")
  public void doSomething() {
    
    // 1분마다 가장 예전에 등록된 연락처를 삭제하는 스케쥴러
    // DELETE - ContactMapper/contactMapper - ContactService/ContactServiceImpl
    
    contactService.deleteOldestContact();
    log.info("doSomething()");
  }
}

contactMapper.java

  public int deleteOldestContact();

추가

ContactServiceImpl

  @Override
  public void deleteOldestContact() {
    contactDao.deleteOldestContact();
  }

정리 빠뜨렸는데 db 가서 식적고 커밋도 해야함

DROP SEQUENCE CONTACT_SEQ;
CREATE SEQUENCE CONTACT_SEQ NOCACHE ORDER;

DROP TABLE CONTACT_T;
CREATE TABLE CONTACT_T (
	CONTACT_NO NUMBER             NOT NULL
  , NAME       VARCHAR2(100 BYTE) NOT NULL
  , TEL        VARCHAR2(100 BYTE) NULL
  , EMAIL      VARCHAR2(100 BYTE) NULL
  , ADDRESS    VARCHAR2(100 BYTE) NULL
  , CREATED_AT VARCHAR2(100 BYTE) NOT NULL
  , CONSTRAINT PK_CONTACT PRIMARY KEY(CONTACT_NO)
);

DELETE FROM CONTACT_T WHERE CONTACT_NO = (SELECT MIN(CONTACT_NO) FROM CONTACT_T);

SELECT * FROM CONTACT_T;

    DELETE
      FROM CONTACT_T
     WHERE CONTACT_NO = (SELECT MIN(CONTACT_NO)
                           FROM CONTACT_T);

7교시

히카리 버전 5.0.1로 바꿈

인터셉터

  1. 컨트롤러로 들어오는 요청과 응답을 가로챈다.

  2. HandlerInterceptor 인터페이스를 구현(권장)하거나, HandlerInterceptorAdapter 클래스를 상속하면 인터셉터가 된다.

  3. Spring Container에 Bean으로 등록해 두어야 한다.

  4. 주요 메소드
    1) preHandle() : 요청 전에 동작할 코드 작성(false를 리턴하면 컨트롤러 요청 동작을 막을 수 있다.) - 얘만 boolean으로 잡을 수 있음
    2) postHandle() : 요청 후에 동작할 코드 작성(컨트롤러의 Handler 동작 이후)
    3) afterCompletion() : View까지 처리가 끝난 후 동작할 코드 작성

  5. 언제 Interceptor가 동작할 것인지 servlet-context.xml에 작성한다. (인터셉터 등록은 servlet-context.xml에 한다)
    1) <mapping> : 인터셉터가 동작할 요청 주소
    2) <exclude-mapping> : 인터셉터가 동작하지 않을 요청 주소
    3) <beans:bean> : 동작할 인터셉터

  6. Filter vs Interceptor


  • 인터셉터 활용 : 로그인이 안되면 글 작성 페이지 못가게 막는다던지 그런 경우

MyInterceptor

@Component
public class MyInterceptor implements HandlerInterceptor {

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    response.setContentType("text/html; charset=UTF-8");
    PrintWriter out = response.getWriter();
    out.println("<script>");
    out.println("alert('인터셉터가 동작했습니다.')");
    out.println("history.back()"); //못들어가게 뒤로 돌려보내기
    out.println("</script>");
    out.flush();
    out.close();
    
    return false;  // 컨트롤러의 요청이 처리되지 않는다.
  }
}
  • 인터셉터의 기본 = 핸들러 인터셉터라고 하는 인터페이스를 구현하는 것
  • 인터셉터의 메소드 (이 중 골라서 사용)

    afterCompletion: 작업 완료 후 실행
    postHandle: 호출 이후 실행
    preHandle: 호출 이전 실행 (가장 많이 사용)
  • preHandle

    특징
  1. request(요청), response(응답) 쓸 수 있다
  2. 반환타입이 boolean, T이면 인터셉터 실행 후 컨트롤러 동작, F이면 인터셉터만 동작하고 컨트롤러는 동작되지 않음.

servlet-context.xml

  <interceptors>
    <interceptor>
      <mapping path="/**"/>
      <exclude-mapping path="/contact/list.do"/>
      <beans:bean class="com.gdu.app12.intercept.MyInterceptor" />
    </interceptor>
  </interceptors>

MyInterceptor로 만든걸 servlet-context에 등록
mapping path -> 언제 동작하는 인터셉터인지 설정

작성페이지로 갈 때 동작하는 인터셉터.


write 페이지로 가는 거 누르면

인터셉터 동작하면서 못 들어간다.

--
/** => 모든 요청

<exclude-mapping path="/contact/list.do"/>

이 요청은 뺀다는 뜻

0개의 댓글