[Web] Layered Structure 실습 - 방명록 구현

spring framework MVC 패턴을 Layered Structure를 사용해 방명록(guestbook) 웹페이지를 만들었다.

- 구조

- 요구사항

  • 방명록 정보는 guestbook table에 (id, 이름, 내용, 등록일)로 저장. id는 자동 입력
  • http://localhost:8080guestbook/을 요청하면 자동으로 /guestbook/list로 redirect한다.
  • 등록 폼을 채우고 '등록'버튼을 누르면 /guestbook/write url로 입력한 값을 전달하여 저장. 저장된 이후에는 /guestbook/list로 리다이렉트 된다
  • 방명록이 없으면 건수는 0으로 출력.
  • 방명록 내용과 폼 사이에 방명록 페이지 링크를 표시. 방명록 5건당 1페이지로 설정
    • 6번째 방명록이 입력되면 페이지 수가 2까지 출력된다. 
    • 1페이지를 누르면 /guestbook/list?start=0을 요청 (=/guestbook/list)
    • 2페이지를 누르면 /guestbook/list?start=5를 요청
  • 방명록에 글을 쓰거나 삭제하면 log 테이블에 (id, client ip address, time, method (등록/삭제)) 데이터가 저장됨. id는 자동 입력


이클립스에서 maven web app 프로젝트를 새로 생성해준다. 역시 제일 먼저 해야 할 일은 각종 플러그인과 dependency 설정. pom.xml과 web.xml 에 스프링 프레임워크와 jstl, json, DB를 사용하기위한 각종 설정을 해준다. 설정을 끝내면 꼭 메이븐 프로젝트를 업데이트 해주고 이클립스를 재시작 해야한다.

프로그램 동작은 아래와 같다.


웹 레이어 설정 파일 : web.xml, WebMvcContextConfiguration.java
비즈니스, 레파지토리 레이어 설정 파일 : ApplciationConfig.java(import->), DBConfig.java



GuestbookController : url mapping control
GuestbookService : 비즈니스 로직을 가지고 있다.
GuestbookServiceImpl : GuestbookService를 실제 구현하고 있는 클래스

LogDto, GuestbookDto - LogDao, GuestbookDao 또한 사용하고있다.
GuestbookDaoSqls : db 에서 사용할 sql문들을 저장하고 있음

전체 구조



* package나 파일명을 지을 때 오타를 내지 않도록 주의, 또 주의하자 .. 나는 패키지명에 오타를 냈다가 몇시간동안 404 오류를 해결하지 못해서 config 파일만 주구장창 봤다. ㅠ
* 예제를 따라서 수행하다 보니 Test.java 파일도 각 패키지 안에 같이 존재하는데 이렇게 하는 것 보다 test용 패키지를 따로 만들어서 테스팅 하는 것이 좋다.

1. Configuration class 작성
'ApplicationConfigjava'

package kr.or.connect.guestbook.config;
 
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
 
@Configuration
@ComponentScan(basePackages = { "kr.or.connect.guestbook.dao""kr.or.connect.guestbook.service" })
@Import({ DBConfig.class })
public class ApplicationConfig {
 
}
cs

* ComponentScan의 basePackages 선언할 때 패키지명을 kr.or... 부터 정확히 명시하지 않으면 제대로 탐색되지 않으므로 주의!

'DBConfig.java'

@Configuration
@EnableTransactionManagement
public class DBConfig implements TransactionManagementConfigurer {
    private String driverClassName = "com.mysql.jdbc.Driver";
 
    private String url = "jdbc:mysql://localhost:3306/connectdb?useUnicode=true&characterEncoding=utf8";
    private String username = "connectuser";
    private String password = "1234";
 
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }
 
    @Override
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        return transactionManger();
    }
 
    @Bean
    public PlatformTransactionManager transactionManger() {
        return new DataSourceTransactionManager(dataSource());
    }
}
cs

* 예제를 따라 해보느라 db 접속 정보가 모두 같은 코드에 있는데 이는 보안상 좋지 못하므로 data 파일을 따로 만들어서 보관해야한다.
* 트랜잭션을 사용하기 위해 TransactionManagementConfigurer를 import 하고 있다.

'WebMvcContextConfig.java'
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "kr.or.connect.guestbook.controller" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {
 
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/css/**").addResourceLocations("/css/").setCachePeriod(31556926);
        registry.addResourceHandler("/img/**").addResourceLocations("/img/").setCachePeriod(31556926);
        registry.addResourceHandler("/js/**").addResourceLocations("/js/").setCachePeriod(31556926);
    }
 
    // default servlet handler를 사용하게 합니다.
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
 
    @Override
    public void addViewControllers(final ViewControllerRegistry registry) {
        System.out.println("addViewControllers가 호출됩니다. ");
        registry.addViewController("/").setViewName("index");
    }
 
    @Bean
    public InternalResourceViewResolver getInternalResourceViewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
}
cs

프로젝트가 실행되면 메인 페이지인 '/WEB-INF/views/ 안에 index.jsp view를 불러오게된다.

2. Dto  만들기

public class Guestbook {
    private Long id;
    private String name;
    private String content;
    private Date regdate;
cs

public class Log {
    private Long id;
    private String ip;
    private String method;
    private Date regdate;
cs

+ getter, setter, toString

3. Dao 만들기

- 필요한 메서드들
1. 전체 방명록 수 조회
2. 전체 방명록 db에서 조회 하여 List로 가져오기
3. 한 건의 방명록 insert
4. id로 하나의 방명록 delete

먼저 필요한 sql문들을 static 상수로 선언한다.
'guestbook.dao.GuestbookSqls.java'

public class GuestbookSqls {
    public static final String DELETE_BY_ID = "DELETE FROM guestbook WHERE id= :id";
    public static final String SELECT_COUNT = "SELECT count(*) FROM guestbook";
    public static final String SELECT_PAGING = "SELECT id,name,content,regdate FROM guestbook ORDER BY id DESC limit :start, :limit";
}
cs

'GuestbookDao.java'

@Repository
public class GuestbookDao {
    private NamedParameterJdbcTemplate jdbc;
    private RowMapper<Guestbook> rowMapper = BeanPropertyRowMapper.newInstance(Guestbook.class);
    private SimpleJdbcInsert insertAction;
    
    public GuestbookDao(DataSource dataSource) {
        this.jdbc=new NamedParameterJdbcTemplate(dataSource);
        this.insertAction = new SimpleJdbcInsert(dataSource).withTableName("guestbook")
                .usingGeneratedKeyColumns("id");
    }
    
    public int deleteById(Long id) {
        Map<String,?> params=Collections.singletonMap("id", id);
        return jdbc.update(DELETE_BY_ID, params);
    }
    
    public Long insert(Guestbook guestBook) {
        SqlParameterSource params = new BeanPropertySqlParameterSource(guestBook);
        return insertAction.executeAndReturnKey(params).longValue();
    }
    
    public List<Guestbook> selectAll(Integer start, Integer limit){
        Map<String, Integer> params = new HashMap<>();
        params.put("start", start);
        params.put("limit",limit);
        return jdbc.query(SELECT_PAGING, params, rowMapper);
    }
    
    public int selectCount() {
        return jdbc.queryForObject(SELECT_COUNT,Collections.emptyMap(),Integer.class);
    }
}
 
cs

* insert는 SimpleJdbcInsert 객체로 이루어진다. id 를 자동으로 생성하는 부분을 주의


'Logdao.java'

@Repository
public class LogDao {
    private NamedParameterJdbcTemplate jdbc;
    private SimpleJdbcInsert insertAction;
 
    public LogDao(DataSource dataSource) {
        this.jdbc = new NamedParameterJdbcTemplate(dataSource);
        this.insertAction = new SimpleJdbcInsert(dataSource).withTableName("log").usingGeneratedKeyColumns("id");
    }
 
    public Long insert(Log log) {
        SqlParameterSource params = new BeanPropertySqlParameterSource(log);
        return insertAction.executeAndReturnKey(params).longValue();
        // insert문을 내부적으로 생성해서 실행하고 자동으로 생성된 id값을 반환하게 된다.
    }
 
}
cs

간단한 insert문만 있으면 된다.

4. Service 만들기

'guestbook.service.GuestbookService.java'

public interface GuestbookService {
    public static final Integer LIMIT=5;
    public Guestbook addGuestbook(Guestbook guestbook, String ip);
    public int deleteGuestbook(Long id, String ip);
    public int getCount();
    public List<Guestbook> getGuestbooks(Integer start);
}
 
cs

'guestbook.service.impl.GuestbookServiceImpl.java'

@Service
public class GuestbookServiceImpl implements GuestbookService {
    @Autowired
    GuestbookDao guestbookDao;
 
    @Autowired
    LogDao logDao;
 
    @Override
    @Transactional(readOnly = false)
    public Guestbook addGuestbook(Guestbook guestbook, String ip) {
        guestbook.setRegdate(new Date());
        Long id = guestbookDao.insert(guestbook);
        guestbook.setId(id);
        Log log = new Log();
        log.setIp(ip);
        log.setMethod("insert");
        log.setRegdate(new Date());
        logDao.insert(log);
 
        return guestbook;
    }
 
    @Override
    @Transactional(readOnly = false)
    public int deleteGuestbook(Long id, String ip) {
        int deleteCount = guestbookDao.deleteById(id);
        Log log = new Log();
        log.setIp(ip);
        log.setMethod("delete");
        log.setRegdate(new Date());
        logDao.insert(log);
        return deleteCount;
    }
 
    @Override
    public int getCount() {
        return guestbookDao.selectCount();
    }
 
    @Override
    @Transactional // readonly
    public List<Guestbook> getGuestbooks(Integer start) {
        List<Guestbook> list = guestbookDao.selectAll(start, LIMIT);
        return list;
    }
 
}
 
cs

* Autowired 어노테이션으로 dao를 사용할 수 있게 한다.
* Transaction 사용을 위해 Transactional 어노테이션을 준다. 내용을 쓰는 트랜잭션에는 디폴트값이 true인 readOnly 옵션에 false를 준다.

5. Controller

'GuestbookController.java'

@Controller
public class GuestbookController {
    @Autowired
    GuestbookService guestbookService;
 
    @GetMapping(path = "/list")
    public String list(@RequestParam(name = "start", required = false, defaultValue = "0"int start, ModelMap model) {
 
        // start로 시작하는 방명록 목록 구하기
        List<Guestbook> list = guestbookService.getGuestbooks(start);
 
        // 전체 페이지수 구하기
        int count = guestbookService.getCount();
        int pageCount = count / GuestbookService.LIMIT;
        if (count % GuestbookService.LIMIT > 0)
            pageCount++;
 
        // 페이지 수만큼 start의 값을 리스트로 저장
        // 예를 들면 페이지수가 3이면
        // 0, 5, 10 이렇게 저장된다.
        // list?start=0 , list?start=5, list?start=10 으로 링크가 걸린다.
        List<Integer> pageStartList = new ArrayList<>();
        for (int i = 0; i < pageCount; i++) {
            pageStartList.add(i * GuestbookService.LIMIT);
        }
 
        model.addAttribute("list", list);
        model.addAttribute("count", count);
        model.addAttribute("pageStartList", pageStartList);
 
        return "list";
    }
 
    @PostMapping(path = "/write")
    public String write(@ModelAttribute Guestbook guestbook, HttpServletRequest request) {
        String clientIp = request.getRemoteAddr();
        guestbookService.addGuestbook(guestbook, clientIp);
        return "redirect:list";
    }
}
cs

6. jsp 파일 생성

'index.jsp'

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
    response.sendRedirect("list");
%>
cs

별다른 역할 없이 list 로 redirect만 하면 된다.

'list.jsp' - 주어진 간단한 형식이 있었지만 제대로 만들어보고 싶어서 css도 넣고 여러가지를 수정했다.
그 결과물,


곧 삭제 버튼도 추가해서 사용 할 예정 ! 

No comments:

Powered by Blogger.