웹 게시물 관리 2. Spring 기본적인 웹 게시물 관리
영속/비즈니스 계층의 CRUD 구현
CRUD는 (Create, Read, Update, Delete) 입니다.
영속 계층의 작업은 항상 다음과 같은 순서로 진행합니다.
- 테이블의 컬럼 구조를 반영하는 VO(Value Object) 클래스의 생성
- MyBatis의 Mapper 인터페이스의 작성/XML 처리
- 작성한 Mapper 인터페이스 테스트
위의 과정 전에 먼저 JDBC 연결을 테스트하는 과정을 거치는 것이 좋습니다.
영속 계층의 구현 준비
거의 모든 웹 애플리케이션의 최종 목적은 데이터베이스에 데이터를 기록하거나, 원하는 데이터를 가져오는 것이 목적이기 때문에 개발할 때 어느 정도의 설계가 진행되면 데이터베이스 관련 작업을 하게 됩니다.
VO클래스의 작성
VO 클래스를 생성하는 작업은 테이블 설계를 기준으로 작성하면 됩니다.
Mapper 인터페이스와 Mapper XML
Mybatis는 SQL을 처리하는데 어노테이션이나 XML을 이용할 수 있습니다. 간단한 SQL이라면 어노테이션을 이용해서 처리하는 것이 무난하지만, SQL이 점점 복잡해지고 검색과 같이 상황에 따라 다른 SQL문이 처리되는 경우에는 어노테이션은 그다지 유용하지 못하다는 단점이 있습니다. XML의 경우 단순 텍스트를 수정하는 과정만으로 처리가 끝나지만, 어노테이션의 경우 코드를 수정하고 다시 빌드 하는 등의 유지 보수성이 떨어지는 이유로 기피는 경우도 종종 있습니다.
Mapper 인터페이스
root-context.xml에 mapper 패키지를 스캔하도록 설정합니다.
<mybatis-spring:scan base-package="패키지명"/>
Mapper 인터페이스를 작성할 때는 리스트(select)와 등록(insert) 작업을 우선해서 작성합니다. 패키지를 작성하고 Mapper 인터페이스를 추가합니다.
Mapper 인터페이스를 작성할 때는 이미 작성된 VO 클래스를 적극적으로 활용해서 필요한 SQL을 어노테이션의 속성값으로 처리할 수 있습니다. (SQL을 작성할 때는 반드시 ';'이 없도록 작성해야 합니다.)
ex) @Select("select * from tbl_board where bno > 0")
이후 SQL Developer에서 먼저 확인을 합니다. 그 이유는 다음과 같습니다.
1) SQL이 올바른지 체크
2) 데이터베이스의 commit을 하지 않았다면 나중에 테스트 결과가 달라지기 때문에 이를 먼저 비교하기 위함
체크가 끝났다면 Mapper 인터페이스를 테스트 할 수 있게 테스트 환경인 'src/test/java'에 mapper 패키지와 동일한 패키지를 작성하고 MapperTests 클래스를 추가합니다.
해당 클래스 안에는
@Setter(onMethod_ = @Autowired)
private BoardMapper mapper;
멤버 변수로 선언합니다.
해당 멤버 변수는 스프링을 통해 인터페이스의 구현제를 주입받아서 동작하게 됩니다.
Mapper XML 파일
위의 테스트가 완료되었다면 src/main/resources 내에 패키지와 동일한 단계의 폴더를 생성하고 XML 파일을 작성합니다.
(폴더를 한 번에 생성하지 말고 하나씩 생성해야 하는 점을 주의해야 합니다.)
파일의 폴더 구조나 이름은 무방하지만 패키지와 클래스 이름을 src/main/java에 선언한 mapper와 동일하게 해주면 나중에 혼란스러운 상황을 피할 수 있습니다.
XML을 작성할 때는 반드시 <mapper>의 namespace 속성값을 Mapper 인터페이스와 동일한 이름을 주는 것에 주의하고, <select> 태그의 id 속성값은 메서드의 이름과 일치하게 작성합니다. resultType 속성의 값은 select 쿼리의 결과를 특정 클래스의 객체로 만들기 위해서 설정합니다. XML에 사용한 CDATA 부분은 XNL에서 부등호를 사용하기 위해서 사용합니다.
mapper 인터페이스이 반환 값은 List형인데 List는 명시할 필요가 없습니다.
위의 XML을 작성하였다면 Mapper 인터페이스에 설정한 SQL 어노테이션은 제거해도 됩니다.
이후 기존의 테스트 코드를 통해서 기존과 동일하게 동작하는지 확인해야 합니다.
영속 영역의 CRUD 구현
웹 프로젝트 구조에서 마지막 영역이 영속 (Persistence) 영역이지만, 실제로 구현을 가장 먼저 할 수있는 영역도 영속 영역입니다. 영속 영역은 기본적으로 CRUD 작업을 하기 때문에 테이블과 VO(DTO) 등 약간의 준비만으로도 비즈니스 로직과 무관하게 CRUD 작업을 작성할 수 있습니다. Mybatis는 내부적으로 JDBC의 PreparedStatement를 활용하고 필요한 파라미터를 처리하는 '?'에 대한 치환은 '#{속성}'을 이용해서 처리합니다.
create(insert) 처리
PK를 Sequence로 활용할 경우 다음과 같은 2가지 방식으로 처리할 수 있습니다.
- insert만 처리되고 생성된 PK 값을 알 필요가 없는 경우
- insert문이 실행되고 생성된 PK 값을 알아야 하는 경우
Mapper 인터페이스에는 위의 상황들을 고려해서 두가지 메서드를 선언합니다.
또한 XML에도 내용을 추가합니다.
@SelectKey는 주로 PK값을 미리(before) SQL을 통해서 처리해 두고 특정한 이름으로 결과를 보관하는 방식입니다.
이후 @Insert 할 때 SQL문을 보면 #{bno}와 같이 이미 처리된 결과를 이용하는 것을 볼 수 있습니다.
resultType은 VO 클래스의 bno의 자료형을 넣었습니다.
작성한 이후 Test클래스에 Test 코드를 작성하고 동작시켜 봅니다.
read(select) 처리
myBatis는 Mapper 인터페이스의 리턴 타입에 맞게 select의 결과를 처리합니다.
MyBatis는 파라미터가 존재하면 해당 인스턴스의 setter를 호출하게 됩니다. Mybatis의 모든 파라미터와 리턴 타입의 처리는 getter, setter의 규칙으로 호출됩니다. 다만, #{속성}이 1개만 존재하는 경우에는 다음 과 같이 별도의 getter 를 사용하지 않고 처리합니다.
mapper 인터페이스에 기본 키로 검색을 하는 메소드 read를 추가합니다.
이후 XML에 다음의 코드를 추가합니다.
테스트 코드를 작성하고 테스트를 수행합니다.
delete 처리
1. PK값을 이용해서 삭제하는 메소드를 mapper 인터페이스에 정의합니다.
2. XML에 해당 쿼리를 작성합니다.
3. Test 코드를 작성하고 실행합니다.
DML 작업은 '몇 건의 데이터가 삭제(혹은 수정)되었는지'를 반환합니다.
리턴타입은 int로 할 수 있습니다.
DML은 resultType를 명시하지 않아도 됩니다.
Select 문일 경우에 해당 쿼리의 결과값을 어떤 형식으로 변환할지 명시해야 합니다.
비즈니스 계층
비즈니스 계층은 고객의 요구사항을 반영하는 계층으로 프레젠테이션 계층과 영속 계층의 중간 다리 역할을 하게 됩니다. 영속 계층은 데이터베이스를 기준으로 해서 설계를 나눠 구현하지만, 비즈니스 계층은 로직을 기준으로 해서 처리하게 됩니다.
예컨대, '쇼핑몰에서 상품을 구매한다'고 가정해 봅니다. 해당 쇼핑몰의 로직이 '물건을 구매한 회원에게는 포인트를 올려준다'고 하면 영속 계층의 설계는 '상품'과 '회원'으로 나누어서 설계하게 됩니다. 반면에 비즈니스 계층은 상품 영역과 회원 영역을 동시에 사용해서 하나의 로직을 처리하게 되므로 다음과 같은 구조를 만들게 됩니다.
설계를 할 때는 원칙적으로 영역을 구분해서 작성해야 합니다. 일반적으로 비즈니스 영역에 있는 객체들은
'서비스(Service)'라는 용어를 많이 사용합니다.
비즈니스 계층을 위해서 org.zerock.service 패키지를 작성합니다.
설계를 할 때 각 계층 간의 연결은 인터페이스를 이용해서 느슨한(loose) 연결(결합)을 합니다.
게시물은 BoardService 인터페이스와 인터페이스를 구현한 BoardServiceImpl 클래스를 선언합니다.
(영속 계층은 Spring에 의해 XML을 참고하여 maaper 인터페이스의 구현체가 자동으로 의존성 주입되었지만 비즈니스 계층은 직접 구현해주어야 합니다.)
Service 메서드를 설계할 때 메서드 이름은 현실적인 로직의 이름을 붙이는 것이 관례입니다. 명백하게 반환해야 할 데이터가 있는 'select'를 해야 하는 메서드는 리턴 타입을 지정할 수 있습니다.
Impl 클래스는 @Service 어노테이션을 필수로 붙여줘야 합니다.
@Service는 계층 구조상 주로 비즈니스 영역을 담당하는 객체임을 표시하기 위해 사용합니다.
Impl 클래스가 정상적으로 동작하기 위해서는 Mapper 객체가 필요합니다.
이는 @Autowired와 같이 직접 설정해 줄 수 있고, Setter를 이용해서 처리할 수도 있습니다.
Lombok을 이용한다면 @Setter(onMethod_ = @Autowired) 와 같이 만들수도 있습니다.
스프링 4.3부터는 단일 파라미터를 받는 생성자의 경우 필요한 파라미터를 자동으로 주입할 수 있습니다.
스프링의 서비스 객체 설정(root-context.xml)
비즈니스 계층의 인터페이스와 구현 클래스가 작성되었다면, 이를 스프링의 빈으로 인식하기 위해서 root-context.xml에 @Service 어노테이션이 있는 패키지를 스캔하도록 추가해야 합니다.
root-context.xml에 namespaces 항목에 context 항목을 추가합니다. 이후 아래 태그를 입력합니다.
<context:component-scan base-package="[service 패키지 명]"></context:component-scan>
namespace를 추가하면 해당 이름으로 시작하는 태그들을 활용할 수 있습니다.
Java 설정의 경우
root-context.xml을 대신하는 RootConfig 클래스를 이용해서 @ComponentScan을 추가합니다.
@ComponentScan(basePackages = {"[패키지 명]"})
비즈니스 계층의 구현과 테스트
src/test/java 밑에 service 패키지명과 동일한 패키지를 만들어주고 테스트 클래스를 생성해 테스트를 진행합니다.
테스트 클래스의 첫 테스트는 정상적으로 Service 객체가 생성되고 Mapper가 주입되었는지 확인합니다.
이후 다음의 작업들을 구현 및 테스트 합니다.
- 등록 작업의 구현과 테스트
- 목록(리스트) 작업의 구현과 테스트
- 조회 작업의 구현과 테스트
- 삭제/수정 구현과 테스트
프레젠테이션(웹) 계층의 CRUD 구현
비즈니스 계층의 구현까지 모든 테스트가 진행되었다면 이제 남은 작업은 프레젠테이션 계층인 웹의 구현입니다.
Controller의 작성
스프링 MVC의 Cnotroller는 하나의 클래스 내에서 여러 메서드를 작성하고, @RequestMapping 등을 이용해서 URL을 분기하는 구조로 작성할 수 있기 떄문에 하나의 클래스에서 필요한 만큼 메서드의 분기를 이용하는 구조로 작성합니다.
과거에는 이 단계에서 Tomcat(WAS)을 실행하고 웹 화면을 만들어서 결과를 확인하는 방식의 코드를 작성해 왔습니다.
이 방식은 시간도 오래 걸리거니와 테스트를 자동화 하기에 어려움이 많으므로 WAS를 실행하지 않고 Controller를 테스트할 수 있는 방법을 학습해야 합니다.
BoardController의 분석
작성하기 전에는 반드시 현재 원하는 기능을 호출하는 방식에 대해 다음과 같이 테이블로 정리한 후 코드를 작성하는 것이 좋습니다.
Task | URL | Method | Parameter | From | URL 이동 |
전체 목록 | /board/list | GET | |||
등록 처리 | /board/register | POST | 모든 항목 | 입력화면 필요 | 이동 |
조회 | /board/get | GET | bno=123 | ||
삭제 처리 | /board/remove | POST | bno | 입력화면 필요 | 이동 |
수정 처리 | /board/modify | POST | 모든 항목 | 입력화면 필요 | 이동 |
화면에 대한 설계는 화면을 구성하는 단계에서 진행할 수 있습니다.
BoardController의 작성
BoardController는 org.zerock.controller 패키지에 선언하고 URL 분석된 내용들을 반영하는 메서드를 설계합니다.
@Controller 어노테이션과 @RequestMapping 어노테이션을 지정해줍니다.
xml: root-context.xml, java: @ComponentScan 를 이용해 스캔설정을 합니다.
목록에 대한 처리와 테스트
BoardContoller는 BoardService 타입의 객체와 같이 연동해야 하므로 의존성에 대한 처리도 같이 진행합니다.
service 객체를 멤버변수로 선언하고 의존성 처리를 진행합니다.
위의 표에 맞게 메소드들을 작성합니다.
src/test/java에 패키지 생성 후 테스트 클래스를 생성하고 테스트를 진행합니다.
이 단계에서 테스트 코드는 기존과 좀 다르게 진행됩니다. 웹을 개발할 때 매번 URL을 테스트 하기 위해서 WAS를 실행하는 불편한 단계를 생략하기 위해서 입니다.
스프링의 테스트 기능을 활용하여 테스트를 진행합니다. WAS를 실행하지 않고도 스프링과 웹 URL을 테스트할 수 있습니다.
MockMvc는 말 그대로 '가짜 mvc'라고 생각하면 됩니다. 가짜로 URL과 파리미터 등을 브라우저에서 사용하는 것처럼 만들어서 Controller를 실행해 볼 수 있습니다. testList는 MockMvcRequestBuilders라는 존재를 이용해서 GET 방식의 호출을 합니다. 이후에는 contorller의 getList()에 의해 반환된 결과를 이용해서 Model에 어떤 데이터들이 담겨 있는지 확인합니다.
Tomcat을 통해서 실행되는 방식이 아니므로 기존의 테스트 코드를 실행하는 것과 동일하게 실행됩니다.
등록 처리와 테스트
Controller 클래스에 register 메소드를 정의합니다. 이후 테스트 코드를 작성하고 테스트를 진행합니다.
테스트 코드는 다음과 같이 작성하였습니다.
조회 처리와 테스트
등록 처리와 유사하게 조회 처리도 BoardController를 이용해서 처리할 수 있습니다.
특별한 경우가 아니라면 조회는 GET 방식으로 처리하고, @GetMapping을 이용합니다.
수정 처리와 테스트
수정 작업은 등록 작업과 유사합니다. 변경된 내용을 수집해서 BoardVO 파라미터로 처리하고, BoardService를 호출합니다. 수정 작업을 시작하는 화면의 경우에는 GET 방시그올 접근하지만 실제 작업은 POST 방식으로 동작합니다.
이때, getViewName은 새로운 페이지의 이름을 가져올때 사용하고, 조회 결과를 가져오려면 getViewMap 메소드를 사용합니다.
삭제 처리와 테스트
삭제 처리도 조회화 유사하게 BoardController와 테스트 코드를 작성합니다. 삭제는 반드시 POST 방식으로만 처리합니다.
경우에 따라서는 Controller에 대한 테스트 코드를 작성하는 것에 대해서 거부감을 가지는 경우도 많습니다. 대부분은 일정에 여유가 없다는 이유로 테스트를 작성하지 않는 경우 많은데 프로젝트를 진행하는 멤버들의 경험치가 낮을수록 테스트를 먼저 진행하는 습관을 가지는 것이 좋습니다. 반복적으로 입력과 수정, WAS의 재시작 시간을 고려해보면 Controller에 대한 테스트를 진행하는 선택이 더 빠른 개발의 결과를 낳는 경우가 많습니다.