심심해서 하는 블로그 :: 'Spring Framework' 카테고리의 글 목록

전체 소스 코드 : https://github.com/ssooni/ssoonidev_blog

Spring Boot : ver 1.5.9

JAVA : ver 1.8 

Build Tool : MAVEN



1. 목표

MongoDB의 데이터를 접근하는 댓글 REST API를 만들어 보는 것이 목표입니다. 추가적으로 Redis Cache Layer를 두어 보는 것도 함께 실습해볼 예정입니다.


2. RestController

Spring 4.0부터 지원하는 @RestController Annotation은 @Controller와 @ResponseBody가 하나로 결합된 Annotation입니다. 따라서 이 클래스 내부의 모든 메서드들의 반환 값은 HTTP Response Body에 직접 쓰이게 됩니다. 따라서 View가 없는 Controller입니다.


대부분 Web 프로그램을 처음으로 개발한다 하면, JSP를 이용해서 화면을 만들다 보니, Web Service는 반드시 View가 있어야 한다고 생각하기 쉽습니다. 하지만 Web Service마다 목적이 있습니다. 사용자에게 View를 제공하는 경우도 있지만, 프로그램끼리 데이터를 주고받기 위해 개발하는 경우도 있습니다. RestController는 후자의 경우에 사용하는 Controller라고 할 수 있습니다. 


이 전 포스팅에서 구현한 ReplyService를 이용해서 RestController를 구현해 보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class ReplyController {
 
    private Logger logger = LoggerFactory.getLogger(ReplyController.class);
    
    @Autowired
    private ReplyService replyService;
    
    @GetMapping("/reply/{bno}")
    public List<ReplyDomain> findByBno(@PathVariable("bno"int bno) throws Exception{
        return replyService.findbyBno(bno);
    }
    // 이하 생략...
}
cs


코드 상에서도 일반 @Controller를 사용했을 때와 차이점을 알 수 있는데, ModelAndView 객체나 String으로 View의 경로를 지정하지 않고 데이터 그 자체를 반환하는 것을 알 수 있습니다. 이후에 실행한다면, 화면에 아래의 그림처럼 JSON 형태의 화면을 보여줄 것입니다. 물론 MongoDB에 데이터가 있는 경우에만 해당됩니다.



3. Swagger

View가 없다 보니 문제가 있습니다. 바로 시연이나 테스트에서 보여주기가 힘들다는 거죠. 데이터를 읽는 것은 그래도 저렇게 화면이라도 나오니까 망정이지만, 데이터를 입력하거나 삭제, 수정하는 경우에는 화면 없이 시연하기 어렵습니다. 그렇다고 이 시연을 위해 View를 새로 만든다는 것은 시간 낭비입니다.  하지만 Swagger를 이용하면 이런 점을 깔끔하게 해결할 수 있습니다. Swagger가 알아서 View를 만들어주기 때문이죠.





Swagger는 프로그램이 시작할 때, URL Mapping을 추적해서 위의 그림과 같은 View를 만들어 줍니다. 그리고 각각의 Tab에는 위의 그림처럼 입력이 가능한 Form과 각각의 타입 등의 정보를 제공해줍니다. 따라서 이 프로그램을 처음 보는 사람도 어떤 URL로 구성되어 있으며, 각각의 URL은 어떤 인자를 받아서 어떻게 반환하는지를 쉽게 확인이 가능합니다. 


pom.xml

1
2
3
4
5
6
7
8
9
10
11
<!-- Swagger2 -->
<dependency>
   <groupId>io.springfox</groupId>
   <artifactId>springfox-swagger2</artifactId>
   <version>2.7.0</version>
</dependency>
<dependency>
   <groupId>io.springfox</groupId>
   <artifactId>springfox-swagger-ui</artifactId>
   <version>2.7.0</version>
</dependency>
cs


이제 개발에 적용하는 방법을 알아보겠습니다. 우선 swagger2와 화면을 보여줄 swagger-ui 라이브러리를 추가합니다. 그리고 Swagger를 설정하기 위한 Configuration Class를 작성합니다. 


SwaggerConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.controller"))
                .paths(PathSelectors.ant("/reply/**")) 
                .build();
    }
}
cs


line 8

컨트롤러가 있는 패키지를 지정할 수 있습니다. 만약 어떤 패키지를 특정하지 않고 모든 패키지를 추적하고 싶다면 매개변수에서 RequestHandlerSelectors.any()를 사용하면 됩니다. 


line 9

Mapping URL 중에서 어떤 URL을 사용할 것인지 정해줄 수 있습니다. 저는 /reply 밑의 모든 경로에 대하여 추적하도록 하였습니다. 마찬가지로 모든 경로를 다 추적하게 하고 싶다면 PathSelectors.any()를 매개변수로 사용하면 됩니다.


이제 Spring Boot 프로그램을 실행하여, http://localhost:8080[/root_path]/swagger-ui.html로 이동하면, 앞서 실행한 결과를 쉽게 확인할 수 있습니다. 


전체 소스코드는 GitHub를 확인해주세요.

다음 포스팅에서는 Redis로 Cache Layer를 구성하는 방법을 포스팅하겠습니다.




,

전체 소스 코드 : https://github.com/ssooni/ssoonidev_blog

Spring Boot : ver 1.5.9

JAVA : ver 1.8 

Build Tool : MAVEN



1. 목표

MongoDB의 데이터를 접근하는 댓글 REST API를 만들어 보는 것이 목표입니다. 추가적으로 Redis Cache Layer를 두어 보는 것도 함께 실습해볼 예정입니다.


2. MongoDB

MongoDB를 처음으로 사용한 건 인턴십 때였습니다. NoSQL의 존재는 이미 알고 있었지만, 이렇게 프로젝트에서 사용은 처음으로 해봤습니다. MySQL이나 오라클을 먼저 접한 저는 테이블이 없고, 난생처음 보는 쿼리에 적응하기 조금 시간이 필요했습니다.


MongoDB vs MySQL


Mongo DB

세상의 모든 데이터가 일정한 틀과 형태를 가지고 있지는 않습니다. 대화나 채팅, 음악 등이 대표적이라 할 수 있죠. 이러한 데이터는 MySQL과 같은 테이블에 저장하기 어렵습니다. 테이블을 만들 때, 하나의 공통 거릴 찾아서 하나의 속성(Column)을 만드는 일이 어렵기 때문이죠. 


Mongo DB는 Collection 안에 Document에 데이터를 저장합니다. Document는 특별한 틀을 가지고 있지 않습니다. Document의 Field 안의 데이터 형식이 서로 안 맞아도 입력이 가능하고, 각 Document들이 모두 일관된 Field를 가지지 않아도 됩니다. 이 방법은 비정형 데이터를 저장하는데 최적의 방법이라고 생각합니다. 관심이 가는 분야이고, Spring Boot 포스팅이 다 끝나는 대로 포스팅을 해보고 싶습니다.


3. Mongo + Spring Boot

전체 폴더 구조나 소스코드는 GitHub를 참고해주세요


pom.xml 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- Redis Cache Layer -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
 
<!-- Swagger2 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.7.0</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.7.0</version>
</dependency>
<!-- Mogno DB -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
cs


spring-boot-starter-data-mongodb

Spring Boot와 Mongo를 연결하기 위해 꼭 필요한 라이브러리입니다. 설정 및 데이터 접근을 하기위한 라이브리입니다. 


spring-boot-starter-data-redis (선택)

다음 포스팅에서 다룰 예정인 Redis Cache를 구현할 때 필요한 라이브러리입니다. 몽고DB만 사용하고 말겠다 생각하는 분이라면 굳이 추가하지 않으셔도 됩니다. 


springfox-swagger2(선택)

Swagger라는 API Document를 자동으로 생성해주는 라이브러리입니다. REST API를 사용하는 경우, 대부분 View가 없어, 테스트를 하거나 시연을 하기 힘든 부분이 있었는데 그것을 해결해준 고마운 라이브러리입니다.


application.properties 작성

1
2
3
4
5
## MongoDB Config ##
spring.data.mongodb.uri=mongodb://localhost:27017
spring.data.mongodb.database=ssooni
spring.data.mongodb.username=ssooni526
spring.data.mongodb.password=ssooni
cs


MongoDB의 uri와 기본적으로 사용할 데이터베이스, 인증을 위한 username과 password 정보를 작성해줍니다. uri에  mongodb://[username]:[password]@[hostname]:[port number]/[database] 처럼 모든 정보를 넣을 수도 있습니다.


MongoConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
public class MongoConfig extends AbstractMongoConfiguration{
 
    @Value("${spring.data.mongodb.username}")
    private String userName;
    
    @Value("${spring.data.mongodb.password}")
    private String password;
    
    @Value("${spring.data.mongodb.database}")
    private String database;
    
    @Override
    protected String getDatabaseName() {
        return database;
    }
 
    @Override
    public Mongo mongo() throws Exception {
        MongoCredential credential = MongoCredential.createCredential(userName, database, password.toCharArray());
        return new MongoClient(new ServerAddress("localhost"27017), Arrays.asList(credential));
    }
    
    public @Bean MongoTemplate mongoTemplate() throws Exception{ 
        return new MongoTemplate(mongo(), database);
    }
    
}
cs


MongoDB와 연결하여 사용하기 위해 설정을 하는 클래스입니다. @Controller나 @Service Annotation처럼 @Configuration Annotation을 사용해, 이 클래스가 무엇인가를 설정하기 위한 용도로 작성된 코드임을 컨테이너에게 알려줍니다. MongoConfig 클래스는 AbstractMongoConfiguration 클래스를 상속하여 구현합니다. 

이때 반드시 getDatabaseName()(line 14)과 mongo()(line 19) 함수를 오버라이드 해야 합니다.


line 4 ~ line 11

application.properties에 정의한 값은 @Value Annotation으로 접근 가능합니다. 


line 19 

mongo() 함수는 Mongo DB에 접근할 객체를 반환하는 함수입니다. 이 때 Mongo에 접근하기 위해 인증정보가 필요한 경우에 MongoCredential 객체에 인증 정보를 담은 후 MongoClient를 생성해줍니다. 


line 24

API 개발 프로젝트에서 @Query Annotation을 사용해서 동적 쿼리를 작성한 적이 있는데,  MySQL 문과 많이 다른 데다가, SpEL은 또 처음이라 고생 많았던 기억이 있습니다. 저처럼 Mongo Query보다 SQL 계열이 익숙하다면 이 mongoTemplete를 이용하여, 최대한 비슷하게 쿼리를 작성할 수 있습니다. 진작에 알았더라면 @Query Annotation 안 썼습니다.(삽질이 소중한 이유이기도 하죠.) DAO 클래스에서 사용하기 위해서 @Bean로 등록하여 DAO 클래스에서 @Autowired로 주입하여 사용이 가능도록 합시다.


ReplyDomain.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@Document(collection="reply")
public class ReplyDomain implements Serializable{
    
    private static final long serialVersionUID = 1L;
 
    @Id
    private String id;
    private int bno;
    private int rno;
    private String contents;
    private String userName;
}
 
cs

@Document Annotation은 Collection 내의 하나의 Document에 대응합니다. Collection 명도 넣어줘서 나중에 어떤 Collection에 접근하여 데이터를 가져오는지를 명시해 줍니다. 


@Data Annotation은 Project Lombok 라이브러리를 Maven에 등록하고 jar 파일로 STS 또는 이클립스에 적용해야 제대로 적용되는 Annotation입니다. getter와 setter, toString() 등을 자동으로 만들어주는 정말 편리한 기능입니다. 개인적으로 getter / setter 자동 완성보다 toString() 자동 완성 기능이 편리했습니다. 로그를 찍을 때, 참 편리하게 데이터를 확인할 수 있기 때문이죠. Lombok을 적용하는 방법도 포스팅으로 소개하겠습니다.


ReplyRepo.java

1
2
3
4
5
public interface ReplyRepo extends MongoRepository<ReplyDomain, Long>{
    public List<ReplyDomain> findByBno(int bno);
    public List<ReplyDomain> findAll();
}
 
cs


두 가지 방법으로 Mongo DB에 데이터를 가져오는 방법을 소개하고자 합니다. 


첫 번째는 위의 ReplyRepo.java 코드처럼 MongoRepository<DataType, SerializeType> 클래스를 상속하여 사용하는 방법입니다. findByBno처럼 사전에 정의된 Action(Find 등등)과 조건(ByBno, All 등등)을 이용하여 함수 이름을 만들어주면 알아서 해당하는 쿼리를 수행합니다. 이 방법은 조건이 매우 단순한 경우에 쉽게 데이터베이스에 접근하는 방법을 제공합니다.  


이 방법으로 정의하기 힘든 복잡한 쿼리를 사용해야 하는 경우에는 @Query Annotation을 사용하여 구현합니다. MongoDB 쿼리에 익숙하다면 이 방법으로 구현하는 것도 좋습니다.


ReplyServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Autowired
private ReplyRepo replyRepo;
    
@Autowired
private MongoTemplate mongoTemplete;
// --- 첫 번째(Repo) 방법 사용시 ----- //
@Override
public List<ReplyDomain> findbyBno(int bno) {
    logger.info("Go Mongo");
    return replyRepo.findByBno(bno);
}
 
// --- 두 번째(Templete) 방법 사용 ----- // 
@Override
public List<ReplyDomain> insert(ReplyDomain reply) {
    int bno = reply.getBno();    
    mongoTemplete.insert(reply);
    return replyRepo.findByBno(bno);
}
 
@Override
public List<ReplyDomain> update(ReplyDomain reply) {
    int bno = reply.getBno();
        
    ObjectId id = new ObjectId(reply.getId());
    Query query = new Query();
    query.addCriteria(Criteria.where("_id").is(id));
        
    Update update = new Update();
    update.set("userName", reply.getUserName());
    update.set("contents", reply.getContents());
    
    WriteResult writeResult = mongoTemplete.updateFirst(query, update, ReplyDomain.class);
    logger.info(writeResult.toString());
            
    return replyRepo.findByBno(bno);
}
cs


두 번째 방법은 앞서 설명한 MongoTemplete를 이용하는 방법입니다. 개인적으로는 두 번째 방법을 선호합니다. Query 클래스와 Criteria 클래스로 조건을 만들고 Update 객체로 수정 할 내용을 넣습니다. 그리고 MongoTemplete 객체의 메서드를 이용하여 실행합니다. 코드를 보다 보면 눈에 익숙한 where 절도 보이고 update에선 set도 보여서, SQL처럼 사용하기가 쉽습니다. 그리고 동적 쿼리가 필요한 경우에는 Java 소스코드 내에서 if문을 이용하면 구현이 쉽습니다간단한 거는 첫 번째로, 복잡한 것은 두 번째로 작성하는 것을 추천합니다. 




다음 포스팅에서는 Swagger를 이용하여 자동으로 API Document를 작성하는 방법을 포스팅하겠습니다.



,

전체 소스 코드 : https://github.com/ssooni/ssoonidev_blog

Spring Boot : ver 1.5.9

JAVA : ver 1.8 

Build Tool : MAVEN


1. 목표

사용자가 View를 통해 데이터를 조회하여, 새로운 데이터를 입력하거나 기존 데이터를 삭제, 수정이 가능한 웹페이지를 작성하는 것이 이번 포스팅의 목표입니다. 


2. controller

Controller는 사용자가 요청하는 URL에 따라, 응답을 해주는 역할을 수행합니다. 그리고 GET, POST, PUT, DELETE 등의 Method로 사용자가 어떤 종류의 요청을 하는지를 정의해  줄 수 있습니다. 따라서 "/board" 같은 주소를 사용하더라도 GET 방식이냐 POST 방식인가에 따라서 사용자에서 다른 응답을 보여줄 수 있습니다. 아래는 이번 게시판 만들기 포스팅에서 사용할 URL의 설계 방안입니다.


/board GET : 홈 화면을 보여준다.

/board POST : 새로운 게시물을 데이터베이스에 저장한다.

/board PUT  : 기존의 게시물을 수정한다.

/board/{bno} DELETE  : {bno}번 게시물을 삭제한다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Controller
public class BoardController {
 
    private static Logger logger = LoggerFactory.getLogger(BoardController.class);
    
    @Autowired
    private BoardService boardService;
 
    @GetMapping("/board")
    public ModelAndView list() throws Exception{
        List<BoardDomain> boardList = boardService.findAll();
        ModelAndView nextPage = new ModelAndView("board/home");
        nextPage.addObject("boardList", boardList);
        return nextPage;
    }
    
    @PostMapping("/board")
    public void create(BoardDomain board) throws Exception{
        logger.info("POST /board : " + board.toString());
        boardService.insert(board);
    }
    
    @PutMapping("/board")
    public void modify(BoardDomain board) throws Exception{
        logger.info("PUT data : " + board.toString());
        boardService.update(board);
    }
    
    @DeleteMapping("/board/{bno}")
    public void delete(@PathVariable("bno"int bno) throws Exception{
        logger.info("DELETE bno : " + bno);
        boardService.delete(bno);
    }    
}
cs


URL 설계를 기반으로 작성한 Controller입니다. @Controller Annotation이 반드시 있어야 Spring Boot에서 웹 요청을 받아들이는 Controller로 이 클래스를 사용하고 beans로 등록합니다. 


URL Mapping

URL을 Mapping 할 때 @RequestMapping(value="/board", method=RequestMethod.GET)처럼 예전에는 조금 길게 작성하는 Annotation 코드를 @GetMapping처럼 짧게 작성할 수 있습니다. @PostMapping@PutMapping, @DeleteMapping도 마찬가지 역할을 수행합니다.


ModelAndView

10번째 줄 list() 함수는 ModelAndView 객체를 반환하는 함수입니다. ModelAndView 객체는 어떤 페이지로 이동하는 페이지 정보와 데이터를 실을 수 있는 객체입니다. 따라서 list() 함수가 수행한 뒤엔 board/list.jsp로 이동하고 이때 boardList라는 객체도 함께 실어서 이동합니다. 이 boardList는 당연히 list.jsp에서 접근 가능합니다.


@PathVariable

URL에 작성된 값을 참조하는 Annotation입니다. tistory 블로그의 URL을 보면 https://ssoonidev.tistory.com/{글번호}로 구성되어 있는데, 이 {글 번호}에 접근하기 위해 사용하는 Annotation이라고 생각하시면 됩니다.  


의존성 주입

데이터베이스에 접근하여 데이터를 처리하는 Service를 @Autowired Annotation을 이용하여 주입합니다. 흔히 Spring Framework에서 의존성 주입(DI)이라는 용어를 사용합니다. 이 개념은 포스팅 하나로 소개해도 무방할 정도로 강력한 개념이므로, 차후 포스팅으로 소개하도록 하고 지금은 간단히 "컨테이너가 미리 만들어진 객체를 가져다가 쓰겠다."라고 생각하면 좋겠습니다.

 

추가적으로 BoardMapper와 BoardService의 코드를 추가/변경하였습니다. 

GitHub의 코드를 꼭 참고하시길 바랍니다.


3. View를 만들자.

지난 포스팅에서 마지막으로 간단하게 뷰를 하나 만들어서 테스트를 수행하였습니다. 테스트 화면에서도 간단하게 Bootstrap을 적용하였는데요. 이번에는 Modal Class를 이용해서 새 글 작성과 수정, 삭제 기능을 하는 뷰를 작성해 보겠습니다. 


전체적인 화면구성

우선 전체적인 화면 구성을 봅시다. home.jsp 파일에 header.jsp 파일을 include 한 형태로 만들고자 합니다. header.jsp는 차후에 Bootstrap의 nav 클래스와 dropdown 클래스 등을 사용하여 검색 기능과 메뉴 이동 기능을 추가할 예정입니다. 시간이 여유롭다면 말이죠.



home.jsp는 게시물을 테이블 형태로 보여주는 화면입니다. 수정과 새 글쓰기 버튼을 누르면 위의 그림과 같은 Modal이 화면에 보이게 구현했습니다. 수정 버튼을 누르면 작성자 명과 작성한 콘텐츠가 텍스트 박스에 위의 오른쪽 그림같이 놓이게 됩니다. 


주요 코드만 간단하게 설명하겠습니다. 

전체 코드는 GitHub를 참고하시길 바랍니다.


home.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<body>
    <div class="container">
        <jsp:include page="../include/header.jsp" />
        <table class="table table-bordered">
            <thead>
                <tr>
                    <th class="col-md-1">bno</th>
                    <th class="col-md-7">contents</th>
                    <th class="col-md-2">userName</th>
                    <th class="col-md-2">수정 / 삭제</th>
                </tr>
            </thead>
            <tbody>
                <c:forEach var="board" items="${boardList}">
                    <tr id="tr${board.bno}">
                        <td>${board.bno}</td>
                        <td><a href="#">${board.contents}</a></td>
                        <td>${board.userName}</td>
                        <td>
                            <div class="btn-group">
                                <button name="modify" value="${board.bno}"
                                    class="btn btn-xs btn-warning">수정</button>
                                <button name="delete" value="${board.bno}"
                                    class="btn btn-xs btn-danger">삭제</button>
                            </div>
                        </td>
                    </tr>
                </c:forEach>
            </tbody>
        </table>
        <jsp:include page="../include/modal.jsp" />
        <button id="createBtn" type="button" class="btn btn-info btn-sm"
            data-toggle="modal">새 글 쓰기</button>
    </div>
</body>
cs


Home화면 jsp 코드입니다. 주소창에 http://localhost:8080/board라고 입력했을 때 보여주는 화면입니다. 


3번째 줄  : header.jsp를 home.jsp에 포함합니다. (경로에 주의바랍니다.)


<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>


14번째 줄 ~ 28번째 줄 (c:foreach Line)

BoardController에서 list() 함수에서 전체 게시물 리스트를 담고 있는 "boardList"라는 List 객체를 전달해주는데,  이 객체를 ${boardList}로 받아 JSP에서 사용 가능합니다. <c:foreach>는 위의 JSP Core Tag Library를 추가하면 사용할 수 있고 for 문과 동일한 역할을 합니다. 즉, boardList 안에 있는 원소들을 board에 하나씩 할당하는 반복문을 수행합니다.  그리고 이 데이터를 테이블에 보여 주기 위해 <tr> <td>를 이용하여 행을 채워

나갑니다.


31번째 줄  : modal.jsp를 home.jsp에 포함합니다.


modal.jsp 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- Modal -->
<div class="modal fade" id="myModal" role="dialog">
    <div class="modal-dialog">
 
        <!-- Modal content-->
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal">&times;</button>
                <h4 id="modal-title" class="modal-title"></h4>
            </div>
            <div class="modal-body">
                <table class="table">
                    <tr>
                        <td>사용자명</td>
                        <td><input class="form-control" id="userName" type="text"></td>
                    </tr>
                    <tr>
                        <td>내용</td>
                        <td><textarea class="form-control" id="contents" rows="10"></textarea></td>
                    </tr>                    
                </table>
            </div>
            <div class="modal-footer">
                <button id="modalSubmit" type="button" class="btn btn-success">Submit</button>
                <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
            </div>
        </div>
    </div>
</div>
cs


Bootstrap Modal 입니다. 하나의 Modal을 이용해서 작성용 Modal과 수정용 Modal을 구현해서 Modal Title (9번째 줄)에 들어가는 내용이 없습니다. 여기에 들어갈 내용은 모두 modal.js에서 해결합니다.


modal.js 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
var action = '';
var url = '';
var type = '';
var bno = 0;
 
$(document).ready(function(){
 
    // 새 글 쓰기 버튼 클릭
    $("#createBtn").click(function(){
        action='create';
        type = 'POST'
        $("#modal-title").text("새 글 작성");
        $("#myModal").modal();
    });
    
    // 수정하기 버튼 클릭
    $("button[name='modify']").click(function(){
        action='modify';
        type = 'PUT';
        bno = this.value;
 
        // content 담기
        var row = $(this).parent().parent().parent();
        var tr = row.children();
        
        var userName = tr.eq(2).text();
        var contents = tr.eq(1).text();
 
        $("#modal-title").text("수정하기");
 
        $("#userName").val(userName);
        $("#contents").val(contents);
        
        $("#myModal").modal();
    });
    
    // 삭제하기 버튼 클릭
    $("button[name='delete']").click(function(){
        bno = this.value;
        $.ajax({
            url : '/board/' + bno,
            type : 'DELETE',
        });
        location.reload();
    })
    
    // Modal의 Submit 버튼 클릭
    $("#modalSubmit").click(function(){
        
        if(action == 'create'){
            bno = 0;
            url = '/board';
        }else if(action == 'modify'){
            url = '/board';
        }
 
        var data = {
            "bno" : bno,
            "userName" : $("#userName").val(),
            "contents" : $("#contents").val()
        };
        
        $.ajax({
            url : url,
            type : type,
            data : data,
success: function(data){ $("#myModal").modal('toggle'); }
complete: function(data){ location.reload(); }
        })
    });
    
 
});
 

 cs


각각의 버튼의 클릭 이벤트를 처리하는 함수들로 구성되어 있습니다. Modal을 보여주는 부분은 $("#myModal").modal() 부분입니다. jQuery에서 # selector는 element의 ID를 찾아 추적합니다. 앞선 modal.jsp에서 modal의 id를 myModal 임을 확인 할 수 있습니다.


또한 눈에 띄는 것은 Ajax 구문입니다.  데이터가 입력 / 수정하면 기존 방식에서는 화면 전체가 reload 됩니다. 변화하는 부분은 테이블 하나인데 전체 페이지가 변경되는 쓸모없는 자원이 낭비합니다. 이 낭비를 줄이기 위해 나온 것이 Ajax입니다 Ajax는 서버에 요청을 자바스크립트에서 하기 때문에 화면 전체가 아닌 일부분만 갱신이 가능합니다.


물론 지금 구현한 함수는 완벽한 Ajax 구문이 아닙니다. 왜냐하면 화면을 전체 Reload 하기 때문에 굳이 Ajax가 아니더라도 해결할 수 있는 부분입니다. 맛보기로 이런 게 있구나 보여주는 것이죠. 이것이 완벽한 Ajax를 구현한 것은 아니라는 점 생각해주시길 바랍니다.


-- 내용 추가 18.08.16 --

borker님의 실행 결과 리플래시가 안되는 문제가 있다해서 확인하였고, MySQL의 insert 요청을 보낸 동시에

화면이 reload해서 새로고침이 안됩니다. success로 Modal 을 닫고, complete로 reload하는 함수를 추가하여 

반영하였습니다. 피드백 해주셔서 감사합니다.


이제 어느 정도 게시판 다운 모습을 하기 시작합니다. 

추가적인 기능을 넣어보는 것은 조금씩 조금씩 애정을 갖고 만들어 나가보겠습니다.

혹시 먼저 만들어 보았으면 하는 기능이 있다면 댓글로 남겨주세요


다음 포스팅은 Mongo DB를 이용해서 REST API를 만드는 것을 해보도록 하겠습니다.

전체 소스 코드는 GitHub를 참조해주시길 바랍니다.



,


전체 소스 코드 : https://github.com/ssooni/ssoonidev_blog

Spring Boot : ver 1.5.9

JAVA : ver 1.8 

Build Tool : MAVEN


1. 목표

사용자가 View를 통해 데이터를 조회하여, 새로운 데이터를 입력하거나 기존 데이터를 삭제, 수정이 가능한 웹페이지를 작성하는 것이 이번 포스팅의 목표입니다.


2. pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
 
<!-- MYSQL -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
 
<!-- JSP / JSTL -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
</dependency>
 
cs


해당 Spring Boot 프로젝트에 필요한 Dependency Library를 추가해 줍니다. 

주요 라이브러리를 간단하게 소개하고 넘어가죠.


mybatis-spring-boot-starter

Mapper라는 아주 좋은 기능을 가지고 있는 Persistence Framework MyBatis를 사용합니다. 

JDBC만으로 코드를 작성하면 PrepareStatement, Connection 등을 사용하여 연결하고, 데이터베이스의 결과를 직접 매핑을 해야 하는 번거로움이 있었는데 @Mapper 와 설정 파일을 이용하면, MyBatis가 매핑까지 해줘서 간단하게 코딩이 가능합니다. 


mysql-connector-java

정형 데이터베이스 MySQL와 연결을 위해 사용하는 라이브러리입니다. Spring Boot의 설정 파일 application.properies에 작성한 URL과 mysql User 정보 등을 읽고 데이터 베이스에 접근합니다.


3. MySQL 테이블 생성

1
2
3
4
5
6
7
create database ssooni;
use ssooni;
create table board(
    bno int auto_increment primary key
    userName varchar(10),
    contents text );
insert into board(userName, contents) values("ssooni""Welcome!!");
cs


(MySQL 설치 및 설정 방법은 추후에 포스팅할 예정입니다.) 

MySQL Workbench나 cmd(또는 bash)를 이용하여 MySQL 데이터베이스에 접속해봅시다.

그 후, 위의 쿼리를 이용해서 데이터베이스를 생성하고 해당 데이터베이스에 사용할 board 테이블을 만들고, 데이터베이스에 Sample 데이터를 입력하는 과정까지 진행해 봅시다.


그리고 테이블 생성과 데이터 입력이 잘 되었는지 확인해 봅시다.

desc [테이블 명] 과 select * from [테이블 명]을 이용하여 위의 그림처럼 조회가 된다면 성공입니다.


4. application.properties 설정

1
2
3
4
5
6
7
8
## View Path Config ## 
spring.mvc.view.prefix=/WEB-INF/view/
spring.mvc.view.suffix=.jsp
 
## Mysql Config ##
spring.datasource.username=ssooni
spring.datasource.password=ssooni
spring.datasource.url=jdbc:mysql://localhost:3306/ssooni?useSSL=false
cs


Spring Boot로 개발을 하면서 가장 괜찮았던 점은 properties 파일에 모든 설정값을 넣어 관리가 가능하다는 점입니다. yaml 파일 형태로도 물론 관리가 가능하며, 이전 프로젝트에서 활용한 경험이 있습니다. 



가장 첫 번째 단락은 jsp 파일이 있는 경로와 확장자를 지정하는 설정입니다. 기존 Spring Framework에서는 xml 파일을 뒤져서 해당 경로를 지정하는 방법을 쓰는데, xml에 비하면 가독성이 아주 좋은 편이라 편리하네요. 참고로 webapp/WEB-INF/view/ 경로는 위의 그림 파일처럼 직접 폴더를 생성해야 합니다. 


두 번째 단락은 MySQL의 URL 정보와 DB 로그인을 위한 ID / PW 정보를 설정합니다. 

URL의 구조는 보통 jdbc:mysql://[ host ip ] : [ host port ] / [ database name ] ? [ option ]으로 구성됩니다. 따라서 지금 저는 localhost의 3306번 포트에 ssooni라는 데이터 베이스에 접근하는 거죠. 

useSSL은 패킷을 암호화하는 Secure Socket Layer의 사용 여부를 설정하는 건데 database에 SSL 설정을 안 했기 때문에 사용하지 않음(false) 옵션을 주었습니다.


5. 설정 끝!! 프로그래밍 하자.

VO 또는 DTO 또는 Domain 클래스 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.example.domain;
 
public class BoardDomain {
    
    private int bno;
    private String userName;
    private String contents;
    
    // GETTER AND SETTER + TOSTRING
    public int getBno() {
        return bno;
    }
    public void setBno(int bno) {
        this.bno = bno;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getContents() {
        return contents;
    }
    public void setContents(String contents) {
        this.contents = contents;
    }
}
 
cs


사람들마다 다르게 명명하지만, 아무튼 비슷하게 사용하고 있는 모델 클래스입니다. 데이터 베이스의 각각의 칼럼에 대응하게 작성하여, 조회 결과를 담을 수 있고 새로운 칼럼을 추가하기 위해 데이터베이스에 데이터를 실을 때 사용하는 클래스입니다. 사실상 getter와 setter, Log에 이쁘게 남기기 위한 toString() 함수로 구성된 아주 간단한 클래스입니다.


Annotation 작성 vs mapper.xml 작성

다음은 Mapper Interface 작성입니다. Mapper Class 하나는 MySQL 데이터베이스의 테이블 하나와 대응합니다. MyBatis는 xml 파일 또는 Annotation을 이용해서 쿼리를 설정하고 데이터베이스에 접근을 합니다. 저는 두 가지 다 이용해 보려고 합니다. 각각의 장단점은 분명히 있으니깐요.


선택 1) Annotation 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Mapper
public interface BoardMapper {
    
    @Select("SELECT * FROM board WHERE bno = #{bno}")
    public List<BoardDomain> findByBno(@Param("bno"int bno);
    
    @Insert("INSERT INTO board(userName, contents) VALUES(#{userName}, #{contents})")
    public void insert(BoardDomain board);
    
    @Update("UPDATE board SET contents=#{contents} where bno=#{bno}")
    public void update(BoardDomain board);
    
    @Delete("DELETE FROM board where bno=#{bno}")
    public void delete(@Param("bno"int bno);
 
}
cs

com.example.dao.BoardMapper.java 코드


MySQL로 할 수 있는 조회(@Select), 생성(@Insert), 갱신(@Update), 삭제(@Delete)를 할 수 있는 코드입니다. @Mapper Annotation과 각각의 함수마다 어떤 쿼리를 수행하는지를 작성하는 것이 끝입니다. 이 방법은 쿼리가 단순한 경우에 사용하면 좋습니다. 


선택 2) Mapper.xml 작성 

1
2
3
4
## Mapper Config ##
mybatis.type-aliases-package=com.example.domain
mybatis.mapper-locations=mapper/**/*.xml

cs


xml 파일로 mapper를 설정하려면 우선 application.properties에 mapper의 위치와 domain 패키지를 추가로 설정해줍니다. domain 패키지는 나중에 mapper.xml에서 resultType을 지정할 때  com.example.domain.BoardDomain 형태가 아닌 BoardDomain으로 간략하게 적기 위해 설정했습니다.

그리고 mapper-locations은 resourse 폴더 안에 mapper 폴더를 만든 후 설정해주시면 됩니다.


 

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd"
 
<mapper namespace="com.example.dao.BoardMapper"
    <select id="findByBno2" parameterType="Integer" resultType="BoardDomain"
        SELECT * FROM borad WHERE bno = #{bno} 
    </select
</mapper>
 
public List<BoardDomain> findByBno(@Param("bno") int bno);
 
/cs

/resource/mapper/BoardMapper.xml(11번째 줄은 제외)


11번 줄의 코드와 같은 결과를 보여주는 xml 파일을 하나 생성하였습니다. 물론 <insert>등도 가능합니다. 가장 위의 DTD를 입력하면 Ctrl + space 자동 완성을 지원해줘서 작성하기 편리해집니다. 


namepspace에는 mapper를 사용할 인터페이스 명을 사용합니다. 저는 선택 1번에서 사용한 BoardMapper.java 파일을 그대로 사용할 예정입니다. 그리고 parameterType과 resultType을 설정해줍니다.


1
2
3
4
5
6
7
8
9
@Mapper
public interface BoardMapper {
    
    @Select("SELECT * FROM board WHERE bno = #{bno}")
    public List<BoardDomain> findByBno(@Param("bno"int bno);
    
    //중간 생략
    public List<BoardDomain> findByBno2(int bno);
}
cs

com.example.dao.BoardMapper.java


아까 작성한 BoardMapper.java에 findByBno2 함수를 다음과 같이 작성했습니다. xml 작성 방법의 장점은 동적 SQL처럼 복잡한 쿼리를 사용할 때 Annotation 보다 훨씬 수월하게 작성할 수 있다는 점입니다.  


Service 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.service;
 
import java.util.List;
 
import com.example.domain.BoardDomain;
 
public interface BoardService {
 
    public List<BoardDomain> findByBno(int bno);
    public List<BoardDomain> findByBno2(int bno);
    public void insert(BoardDomain board);
    public void update(BoardDomain board);
    public void delete(int bno);
 
}
cs

com.example.service.BoardService.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.example.service.impl;
 
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import com.example.dao.BoardMapper;
import com.example.domain.BoardDomain;
import com.example.service.BoardService;
 
@Service
public class BoardServiceImpl implements BoardService{
 
    
    @Autowired
    BoardMapper boardMapper;
    
    @Override
    public List<BoardDomain> findByBno(int bno) {
        return boardMapper.findByBno(bno);
    }
        
    @Override
    public List<BoardDomain> findByBno2(int bno) {
        return boardMapper.findByBno2(bno);
    }
 
    @Override
    public void insert(BoardDomain board) {
        boardMapper.insert(board);
    }
 
    @Override
    public void update(BoardDomain board) {
        boardMapper.update(board);
    }
 
    @Override
    public void delete(int bno) {
        boardMapper.delete(bno);
    }
    
}
cs

com.example.service.Impl.BoardServiceImpl.java


"아니.. DAO랑 똑같은데 굳이 또 해야 해???" 

이런 질문을 주변에서 간혹 받기도 합니다. 앞서 말했듯이 DAO는 테이블 당 하나씩 연결되는 것이라면 Service는 비즈니스 또는 기능당 하나씩 사용하는 겁니다. 지금은 Service 클래스가 데이터 베이스의 테이블을 하나 밖에 안 쓰는 경우이지만 , 하나의 기능 상에 테이블을 두 개 이상 사용하는 경우가 실제 프로젝트에서 상당히 많습니다. 예를 들면 게시물을 작성하는 동시에 게시물 History 테이블에 이력을 저장하는 경우가 대표적입니다.


6. Test

여기까지 수행하면 데이터베이스에 데이터를 읽는 코드를 작성해서 데이터를 잘 불러오는지 테스트를 진행해보겠습니다. controller 패키지와 그 안에 BoardController Class를 생성하고 아래 코드를 작성해 봅시다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.servlet.ModelAndView;
 
import com.example.domain.BoardDomain;
import com.example.service.BoardService;
 
@Controller
public class BoardController {
 
    @Autowired
    BoardService boardService;
    
    @GetMapping("/test")
    public ModelAndView readTest() {
        List<BoardDomain> boardList = boardService.findByBno(1);
        ModelAndView nextPage = new ModelAndView("board/readTest");
        nextPage.addObject("boardList", boardList);
        return nextPage;
    }
}
cs
com.example.controller.Boardcontroller.java

그리고 화면을 보여줄 JSP 파일도 만들어 줍니다. view 폴더 안에 board폴더를 생성한 후 readTest.jsp 파일을 생성해 아래와 같이 작성해 줍니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
    "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 
<link rel="stylesheet"
    href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
 
<title>Test Read Data</title>
</head>
<body>
    <div class="container">
        <h2>Board</h2>
        <table class="table table-bordered">
            <thead>
                <tr>
                    <th>bno</th>
                    <th>contents</th>
                    <th>userName</th>
                </tr>
            </thead>
            <tbody>
                <c:forEach var="board" items="${boardList}">
                    <tr>
                        <td>${board.bno}</td>
                        <td><a href="#">${board.contents}</a></td>
                        <td>${board.userName}</td>
                    </tr>
                </c:forEach>
            </tbody>
        </table>
    </div>
</body>
</html>
cs


결 과 


다음 포스팅에서는 Controller와 View에 대하여 알아보는 시간을 갖도록 해볼게요.

도움이 되셨으면 좋겠습니다.

 

,