spring boot 웹 애플리케이션을 작성할거야.
DBMS 는 postgresql 을 사용할거고, DB 프로그래밍은 mybatis 를 사용할거야.
html 은 thymeleaf 를 사용해보겠어.
lombok 은 기본적으로 들어가주는게 좋을 것 같아.
프로젝트 구성
https://start.spring.io/ 에서 프로젝트를 구성하고 GENERATE 버튼으로 프로젝트 압축파일을 다운로드받을 수 있지.
프로젝트 열기
IDE 는 IntelliJ 를 사용하겠어.
프로젝트 실행
프로젝트를 오픈해서 바로 실행시켜보면 아래와 같이 메시지가 표시되지.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.3)
2024-02-25T18:31:59.489+09:00 INFO 4604 --- [ main] c.woohahaapps.study.diary.Application : Starting Application using Java 17.0.10 with PID 4604 (D:\_MyProject\springbootstudy\study.diary\build\classes\java\main started by woohaha in D:\_MyProject\springbootstudy\study.diary)
2024-02-25T18:31:59.495+09:00 INFO 4604 --- [ main] c.woohahaapps.study.diary.Application : No active profile set, falling back to 1 default profile: "default"
2024-02-25T18:32:01.686+09:00 WARN 4604 --- [ main] o.m.s.mapper.ClassPathMapperScanner : No MyBatis mapper was found in '[com.woohahaapps.study.diary]' package. Please check your configuration.
2024-02-25T18:32:02.529+09:00 INFO 4604 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2024-02-25T18:32:02.558+09:00 INFO 4604 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2024-02-25T18:32:02.558+09:00 INFO 4604 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.19]
2024-02-25T18:32:02.618+09:00 INFO 4604 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2024-02-25T18:32:02.618+09:00 INFO 4604 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2979 ms
2024-02-25T18:32:03.129+09:00 WARN 4604 --- [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception with message: Failed to determine a suitable driver class
2024-02-25T18:32:03.131+09:00 INFO 4604 --- [ main] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
2024-02-25T18:32:03.155+09:00 INFO 4604 --- [ main] .s.b.a.l.ConditionEvaluationReportLogger :
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2024-02-25T18:32:03.199+09:00 ERROR 4604 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class
Action:
Consider the following:
If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).
Process finished with exit code 1
dataSource 가 정의되어 있지 않다고 그러네.
프로젝트 설정 (datasource)
가장 먼저 작성해야 할 내용은 DBMS(PostgreSQL) 연결 정보이지.
그 전에 PostgreSQL 에 데이터베이스부터 생성해야겠네. 따라서 생성해야 할 것은 해당 데이터베이스에 접근할 수 있는 사용자계정과 패스워드지.
psql 로 postgres 에 접속하자.
; 사용자계정 생성
postgres=# create user diary password 'diary_pw';
; 데이터베이스 생성
postgres=# create database diary owner diary;
데이터베이스 Tool 을 사용하여 새로 생성한 데이터베이스에 접속 테스트하자.
이제 데이터베이스 연결 정보를 application.properties 파일에 등록하자. spring.datasource.을 치고나면 여러 항목들이 listing 된다.
spring.datasource.url=jdbc:postgresql://localhost:5432/diary
spring.datasource.username=diary
spring.datasource.password=diary_pw
spring.datasource.url 속성값의 구성
jdbc:postgresql://localhost:5432/diary
jdbc:<DBMS타입>://<ServerAddress>:<Port>/<DatabaseName>
application.properties 는 yml 형식으로도 사용이 가능해.
properties 확장자를 yml 로 변경하고 아래 형식대로 입력하면 돼. 중복적인 코드를 입력할 필요가 없는게 장점이네.
spring:
datasource:
url: jdbc:postgresql://localhost:5432/diary
username: diary
password: diary_pw
데이터베이스 설계
일기장 프로그램을 만들어볼 생각이야. 아주 기본적인 데이터만으로 구성해볼께. 컬럼 구성은 차근차근 필요할 때마다 추가하는 걸로.
하루하루의 내용을 작성해서 저장할 테이블 diary.
일단 일기작성날짜 컬럼이 필요하고, 일기작성내용 컬럼이 필요하겠네.
TableName: diary
Column : diary_date (Date)
Column : diary_content (Text)
아래와 같이 PostgreSQL 데이터베이스에 테이블 diary 를 생성했어. (pgadmin 4를 사용해서)
각 항목의 고유값을 serial Data type 으로 id 라는 컬럼을 지정했어. serial Data type 은 1 ~ 2,147,483,647 까지의 값 범위를 저장할 수 있고, 직접 값을 입력하지 않아도 자동증가되는 값이 저장돼. 고유값이므로 Not NULL, Primary key 로 지정했어.
id 라는 컬럼을 추가한 이유는 같은 날짜에 여러번의 일기를 쓸 수도 있을 것 같아서야. 이럴 때는 diary_date 값(Type:date) 이 동일할것이기 때문에 id 로 구분할 수 있어야만 하지. diary_date 는 일부러 시간값을 뺀 값만 저장되도록 date type 으로 지정했어.
일기내용을 저장할 diary_content 컬럼은 text Data type 으로 지정해서 매우 큰 데이터도 저장이 가능하게 했어.
위와 같은 설정은 아래와 같은 DDL 을 만들어내지.
CREATE TABLE public.diary
(
id serial NOT NULL,
diary_date date NOT NULL,
diary_content text,
PRIMARY KEY (id)
)
TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.diary
OWNER to diary;
COMMENT ON TABLE public.diary
IS '일기장';
테스트로 데이터를 입력해볼께.
insert into diary (diary_date, diary_content) values (DATE '2024-02-26', '기분좋은 하루');
id 컬럼값을 직접 지정하지 않았는데도 1 이라는 값이 자동으로 입력된 것을 확인할 수가 있어.
date 컬럼에 값을 입력할 때는 문자열형의 날짜값 앞에 DATE 함수이름을 붙여줘야 해.
동일한 값으로 또 데이터를 입력해보면 id 값이 증가하는걸 알 수가 있지.
Rest API 작성 (Controller Class)
데이터베이스에 대한 구성이 완료되었으니까 여기에 연결하기 위한 Rest API 작성부터 해볼께.
url 을 통한 데이터베이스 연결 작업의 시작인거지.
빨간색 사각형 부분에 대해서는 아직 작성해야 할 것이 많지만, 대략의 흐름도는 위 그림과 같아.
웹브라우저를 이용해서 Rest API 를 호출하면 아직 설명하지 않은 흐름을 따라 database 에서 데이터를 가져오기도 하고, database 에 데이터를 집어넣기도 하지.
Rest API 로 작성할 기본적인 내용은 CRUD (Create/Read/Update/Delete) 야.
src/main/java/com.woohahaapps.study.diary 아래에 controller package를 생성하고, 그 안에 DiaryController 클래스를 생성해볼께.
DiaryController 클래스에 RestController 클래스화시키기 위한 기본 뼈대를 붙여볼께.
package com.woohahaapps.study.diary.controller;
import org.springframework.web.bind.annotation.*;
@RestController
public class DiaryController {
// Create
@PostMapping("/diary")
public void CreateDiary(String date, String content) {
}
// Read
@GetMapping("/diary")
public void GetDiary(Integer id) {
}
// Update
@PutMapping("/diary")
public void UpdateDiary(Integer id, String date, String content) {
}
// Delete
@DeleteMapping("/diary")
public void DeleteDiary(Integer id) {
}
}
여러가지를 단계적으로 수정해나갈 예정이니 최종적인 코드는 아니라고 생각해줘.
일단 @RestController 애노테이션으로 DiaryController 클래스가 RestAPI 를 제공하기 위한 컨트롤러 클래스로 정의되도록 해준그야.
그 안에는 CRUD 에 해당하는 함수의 기본 뼈대를 작성해봤어.
일단 모든 함수의 진입 url 은 /diary 로 시작이 되도록 작성했는데, CRUD 각각에 맞게 이 url 은 변경이 필요해.
Create 에 해당하는 함수 CreateDiary 는 날짜(date)와 일기내용(content)을 입력받고, Read 에 해당하는 GetDiary 는 고유번호(id)를 입력받아. Update 는 고유번호(id), 날짜(date)와 일기내용(content)를 입력받고, Delete 는 고유번호(id)를 입력받아.
Read 기능으로 특정 날짜의 모든 일기를 읽어오기 위한 함수도 당장 준비할 수 있으면 좋겠지만, 일단 가장 기본적인 코드를 작성해보고 난 다음에 추가로 작성해볼께.
RestAPI 는 http://localhost:8080/diary URL 로 접근하게 될거야. CRUD 각각 method 가 Post, Get, Put, Delete 가 사용되지. 각 method 마다 기본 url 에 값을 사용하는 방법이 달라져.
CreateDiary (Method:POST)
가장 먼저 POST method 를 사용한 CreateDiary 함수를 살펴보자. 파라미터는 date, content 를 입력받게 되어 있어. 만약 GET 방식(url에 파라미터를 포함시키는 방식)이라면 http://localhost:8080/diary?date=2024-02-26&content=기분좋은 하루 와 같이 작성하면 date, content 파라미터로 받아들일 수 있을거야. 확인을 한번 해보자.
@PostMapping("/diary")
public void CreateDiary(String date, String content) {
System.out.println("date=" + date + ",content=" + content);
}
RestAPI 사용테스트를 하기 위해서 Postman 도구를 사용해볼께. (https://www.postman.com/)
method 를 POST 로 선택했지만, Params 에 작성했기 때문에 GET 방식처럼 URL 이 완성되었어. 이 URL 호출에 따라 아래와 같이 로그가 남게 되지.
POST method 를 사용하기 위해서는 Body 영역으로 데이터를 전달해 주어야 해. 그래서 Postman 의 Body 데이터를 아래와 같이 작성했어.
Postman 에서 Send 버튼을 눌러서 서버에 남겨지는 로그를 보니 정상적으로 데이터가 들어오기는 하네.
즉, GET 방식이든, POST 방식이든 파라미터 이름과 동일한 Key 값으로 데이터가 들어오면 처리가 가능하지.
그런데, 서버로 데이터를 올릴 때 보통 html 의 Form 태그를 사용하잖아?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Diary Form</title>
</head>
<body>
<form action="/diary" method="POST">
<label for="date">날짜:</label>
<input type="text" name="date" />
<label for="content">내용:</label>
<textarea rows="10" name="content">
</textarea>
<input type="submit" value="저장" />
</form>
</body>
</html>
/resources/static 아래에 diaryform.html 파일을 생성하고 위 코드를 입력해볼께.
웹브라우저에 http://localhost:8080/diaryform.html 이라고 입력하면 일기 내용을 입력할 수 있는 폼이 표시가 되지.
날짜와 내용을 입력하고 “저장” 버튼을 클릭하면 서버 로그에 입력받은 내용이 잘 표시되는걸 볼 수가 있어.
Service
일단 웹브라우저에서 데이터를 입력하고 Rest API 를 호출하면 서버까지 데이터가 정상적으로 전달되는걸 확인했지. 이제 이 데이터를 DB 까지 전달시키는 과정을 작성해볼께.
Rest API 가 직접 database 에 연결되진 않아. 중간에 Service 계층을 두고, 이 Service 계층에서 비지니스 로직을 구현하지.
src/main/java/com.woohahaapps.study.diary 아래에 service 패키지를 만들고 그 아래에 DiaryService 클래스를 생성해볼께. 이 클래스에는 @Service 애노테이션을 붙여줘서 spring boot 가 Service 로 인식하게끔 해줘야 해.
package com.woohahaapps.study.diary.service;
import org.springframework.stereotype.Service;
@Service
public class DiaryService {
public void CreateDiary(String date, String content) {
System.out.println("Service:date=" + date + ",content=" + content);
}
}
일단은 일기를 생성하기 위한 메소드만 추가해봤어. 파라미터로는 날짜(date)와 일기내용(content)을 받고 있어.
Rest API Controller 에서 DiaryService 의 CreateDiary 메소드를 사용해볼께.
package com.woohahaapps.study.diary.controller;
import com.woohahaapps.study.diary.service.DiaryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
public class DiaryController {
private final DiaryService diaryService;
@Autowired
public DiaryController(DiaryService diaryService) {
this.diaryService = diaryService;
}
// Create
@PostMapping("/diary")
public void CreateDiary(String date, String content) {
System.out.println("date=" + date + ",content=" + content);
diaryService.CreateDiary(date, content);
}
...
}
웹브라우저에서 http://localhost:8080/diaryform.html 을 호출해서 내용을 입력하고 저장하면 아래와 같은 서버로그가 남겨지지.
Rest API 를 통해서 Service 까지 데이터가 전달되어지는 과정을 작성하고 그 결과를 확인해봤어.
이제 mybatis 를 이용해서 database 에 데이터가 저장되도록 설정해볼께.
mybatis 설정
mybatis 는 비지니스로직이 구현되어 있는 Service 계층과 database 사이에서 데이터 액세스(읽기, 저장, 수정, 삭제) 등의 기능을 담당하는 계층이야. mybatis 계층에 해당하는 클래스에는 @Mapper 라는 애노테이션을 붙여주지. 이건 JDBC 나 JPA 에서 @Repository 애노테이션과 비슷한 용도야.
src/main/java/com.woohahaapps.study.diary 아래에 mapper 라는 패키지를 만들고 DiaryMapper 라는 인터페이스를 만들어볼께. 앞에서는 계속 클래스를 만들어왔지만, 이번에는 특별하게 인터페이스인 점을 확인해줘.
DiaryMapper 인터페이스에 작성한 코드를 살펴볼께. 이번에도 일기작성을 위한 API인 CreateDiary 하나만을 작성해봤어. 나머지는 뒤에서 추가해볼께.
package com.woohahaapps.study.diary.mapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DiaryMapper {
public void CreateDiary(String date, String content);
}
@Mapper 라는 애노테이션을 붙이면 spring boot 가 해당 인터페이스에 대한 구현체를 자동으로 작성해줘. ServiceDiary 클래스에서 이 인터페이스를 사용하는 코드를 작성해볼께.
package com.woohahaapps.study.diary.service;
import com.woohahaapps.study.diary.mapper.DiaryMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class DiaryService {
private final DiaryMapper diaryMapper;
@Autowired
public DiaryService(DiaryMapper diaryMapper) {
this.diaryMapper = diaryMapper;
}
public void CreateDiary(String date, String content) {
System.out.println("Service:date=" + date + ",content=" + content);
diaryMapper.CreateDiary(date, content);
}
}
여기까지 작성한 상태에서 앞에서 한 것처럼 웹브라우저에서 http://localhost:8080/diaryform.html 을 로드하고 내용을 입력한 후에 “저장” 버튼을 누르면 아래처럼 에러가 발생할거야.
2024-02-26T16:01:29.661+09:00 ERROR 23132 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.woohahaapps.study.diary.mapper.DiaryMapper.CreateDiary] with root cause
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.woohahaapps.study.diary.mapper.DiaryMapper.CreateDiary
at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:229) ~[mybatis-3.5.14.jar:3.5.14]
at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:53) ~[mybatis-3.5.14.jar:3.5.14]
at org.apache.ibatis.binding.MapperProxy.lambda$cachedInvoker$0(MapperProxy.java:96) ~[mybatis-3.5.14.jar:3.5.14]
at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708) ~[na:na]
at org.apache.ibatis.util.MapUtil.computeIfAbsent(MapUtil.java:36) ~[mybatis-3.5.14.jar:3.5.14]
at org.apache.ibatis.binding.MapperProxy.cachedInvoker(MapperProxy.java:94) ~[mybatis-3.5.14.jar:3.5.14]
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:86) ~[mybatis-3.5.14.jar:3.5.14]
at jdk.proxy2/jdk.proxy2.$Proxy66.CreateDiary(Unknown Source) ~[na:na]
at com.woohahaapps.study.diary.service.DiaryService.CreateDiary(DiaryService.java:18) ~[main/:na]
at com.woohahaapps.study.diary.controller.DiaryController.CreateDiary(DiaryController.java:20) ~[main/:na]
...
mybatis 에서 발생하는 exception 이고 뭔가 바인딩이 안되었다는 뜻인것 같네. 있어야 할 게 없다는 의미지.
뭐가 없어서 발생하는거냐면, @Mapper 애노테이션을 통해서 구현된 구현체가 데이터베이스에 대해서 실행해야 할 쿼리를 찾을 수 없다는 얘기인거야.
mybatis 는 Mapper 인터페이스와 이 인터페이스에 정의되어 있는 함수가 사용할 Sql Command 가 준비되어 있어야 해. 그런데 아직 Sql Command 를 준비해놓지 않은 상태인거지.
인터페이스 API 함수들이 각각 사용할 Sql Command 는 xml 형태로 준비를 해주지.
src/main/resources 아래에 mapper 라는 디렉토리를 만들고 그 안에 DiaryMapper.xml 파일을 만들어볼께.
Sql Command 를 작성해주는 xml 파일의 기본 뼈대 코드는 아래와 같이 작성해주면 돼.
<?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.woohahaapps.study.diary.mapper.DiaryMapper">
<insert id="CreateDiary">
select now()
</insert>
</mapper>
mapper 노드의 namespace 속성값으로는 @Mapper 애노테이션을 붙여준 인터페이스의 전체 패키지명(인터페이스 이름 포함)을 적어주면 돼.
그 안에 SQL 문을 기록해주면 되는데 CRUD 별로 insert, select, update, delete 라는 노드 이름을 사용하면 되고, 각 노드의 id 는 매퍼 인터페이스에서 선언한 함수를 기록해주면 되지.
그런데 이 상태로는 mybatis 가 DiaryMapper.xml 의 존재를 알 수가 없기 때문에 이 존재를 알리기 위한 환경을 추가로 작성해 주어야 해.
src/main/resources 아래에 mybatis-config.xml 이라는 이름의 파일(이름은 임의로 정해도 돼)을 생성하고, Sql Command 를 정의한 xml 파일의 경로와 mybatis 관련 설정값을 기록해주면 돼.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<mappers>
<mapper resource="mapper/DiaryMapper.xml"/>
</mappers>
</configuration>
configuration / mappers 아래에 mapper 노드에 resource 속성값으로 src/main/resources 하위의 매퍼 xml 파일의 경로를 적어주면 되지.
그리고 이 설정파일 경로를 application.properties 나 application.yml 에 기록해주면 돼.
application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/diary
username: diary
password: diary_pw
mybatis:
config-location: classpath:mybatis-config.xml
application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/diary
spring.datasource.username=diary
spring.datasource.password=diary_pw
mybatis.config-location=classpath:mybatis-config.xml
이제 웹브라우저에 http://localhost:8080/diaryform.html 을 로드해서 데이터를 입력한 후에 “저장” 버튼을 눌러도 에러가 발생하지 않지.
그런데, DiaryMapper.xml 파일에서 select now() 라는 SQL 문을 작성해 둔 상태라, diary 테이블에 레코드가 insert 되진 않아. 이제 insert 쿼리문을 작성하는 방법과 쿼리문에 파라미터를 대입시키는 방법에 대해서 살펴볼거야.
@Mapper
public interface DiaryMapper {
public void CreateDiary(String date, String content);
}
이 파라미터를 SQL 문에서 사용할 때는 #{변수명} 과 같은 형식으로 사용해야 해.
<mapper namespace="com.woohahaapps.study.diary.mapper.DiaryMapper">
<insert id="CreateDiary">
insert into diary (diary_date, diary_content) values (DATE(#{date}), #{content});
</insert>
</mapper>
#{date} 와 #{content} 는 파라미터 쿼리화되어 CreateDiary 함수로 전달된 파라미터값이 대입이 되지.
이렇게까지 수정한 후에 http://localhost:8080/diaryform.html 을 실행시켜보면 diary 테이블에 새로운 데이터가 잘 입력되는걸 확인할 수 있어.
이제 나머지 기능들을 추가시켜볼께.
GetDiary (Method:GET)
GetDiary 는 diary 데이터를 얻기 위해서 사용하겠지. diary 데이터는 id, date, content 로 구성되지. 이 구성요소를 하나의 클래스로 구성해볼께.
src/main/java/com.woohahaapps.study.diary 패키지 아래에 domain 패키지를 추가하고 Diary 클래스를 추가해볼께.
package com.woohahaapps.study.diary.domain;
import lombok.Data;
import java.time.LocalDate;
@Data
public class Diary {
private Integer id;
private LocalDate date;
private String content;
}
3개의 구성요소를 멤버로 갖는 Diary 클래스를 @Data 애노테이션으로 선언해줬어. @Data 애노테이션은 @Getter, @Setter, @RequiredArgsConstructor, @ToString, @EqualsAndHashCode, @Value 애노테이션을 모두 합친 애노테이션이야.
이제 DiaryController 의 GetDiary 함수의 리턴값을 Diary 로 수정해볼께.
DiaryController.java
@RestController
public class DiaryController {
...
// Read
@GetMapping("/diary/{id}")
public Diary GetDiary(@PathVariable("id") Integer id) {
return diaryService.GetDiary(id);
}
...
}
기존 URL 은 “/diary” 였는데, /{id} 를 추가했어. 특정 id 값의 데이터를 받기 위해서 파라미터가 필요하기 때문이지, 이 {id} 값을 파라미터로 전달하기 위해서는 @PathVariable 이라는 애노테이션이 필요하지. url 에 붙인 변수명을 붙이면 해당 값이 파라미터로 전달되는 방식이야.
url 경로를 “/diary/{diaryid}” 라고 해주면 @PathVariable(“diaryid”) 로 수정하면 되고, GetDiary 함수로는 id 변수로 {diaryid} 에 붙은 변수값이 전달되지.
DiaryService 클래스의 GetDiary 가 Diary 를 리턴하도록 작성하면 되겠네.
DiaryService.java
@Service
public class DiaryService {
...
public Diary GetDiary(Integer id) {
return diaryMapper.GetDiary(id);
}
}
DiaryMapper 인터페이스에도 GetDiary 함수를 선언해줄께.
DiaryMapper.java
@Mapper
public interface DiaryMapper {
public void CreateDiary(String date, String content);
public Diary GetDiary(Integer id);
}
마지막으로 DiaryMapper.xml 에 GetDiary 함수의 기능에 맞는 쿼리를 작성해볼께.
<mapper namespace="com.woohahaapps.study.diary.mapper.DiaryMapper">
...
<select id="GetDiary" resultType="com.woohahaapps.study.diary.domain.Diary">
select
id, diary_date as date, diary_content as content
from diary
where id=#{id};
</select>
</mapper>
select 쿼리가 리턴하는 데이터형을 resultType 속성값으로 적어주면 되는데, 반드시 풀 패키지명을 기록해야 해.
만약 properties 파일에 아래와 같은 속성을 기록해준다면 클래스 이름만 기록해도 되지.
mybatis:
config-location: classpath:mybatis-config.xml
type-aliases-package: com.woohahaapps.study.diary.domain
쿼리문에서 id 를 제외한 컬럼명을 Diary 클래스의 멤버명으로 alias 를 해주어야만 mybatis 가 각 멤버를 매핑해서 저장해주지.
아니면 Diary 클래스의 멤버명을 테이블 컬럼명에 맞게 네이밍해도 돼.
예를 들어, 컬럼명이 diary_date 이면 Diary 클래스의 멤버명도 동일하게 diary_date 라고 해주는거지. 이렇게 하면 select 쿼리문에서 각 컬럼명에 대한 alias 를 붙이지 않아도 돼.
@Data
public class Diary {
private Integer id;
private LocalDate diary_date;
private String diary_content;
}
DiaryMapper.xml
...
<select id="GetDiary" resultType="com.woohahaapps.study.diary.domain.Diary">
select
*
from diary
where id=#{id};
</select>
</mapper>
웹브라우저 주소창에 http://localhost:8080/diary/1 이라고 입력하면 아래와 같이 id 1에 해당하는 데이터가 json 데이터 구조로 표시가 되지.
UpdateDiary (Method:PUT)
이번에는 데이터 수정 방법에 대한 코드를 작성해볼께.
데이터를 수정하기 위해서 기존의 데이터를 불러온 뒤에 표시하고, 수정된 내용을 다시 서버로 전송해서 저장하는 방식으로 구현해볼께.
기존 데이터를 불러서 form 양식에 표시하기 위한 Rest API 를 하나 추가할께. 데이터를 화면에 표시하기 위한 방법으로 html template 을 사용할건데, thymeleaf 를 사용할께.
https://start.spring.io 에서 ADD DEPENDENCIES 버튼을 클릭하고 thymeleaf 를 입력하면 돼.
Dependencies 목록에 Thymeleaf 가 추가된 것을 확인하고 EXPLORE 버튼을 클릭하면 build.gradle 내용을 확인할 수가 있지.
build.gradle 에서 새로 추가된 implementation ‘org.springframework.boot:spring-boot-starter-thymeleaf’ 을 복사해서 프로젝트의 build.gradle 에 붙여넣고 gradle 을 refresh 하면 돼.
타임리프(thymeleaf) 문법을 사용해서 일기내용을 편집하고 저장하기 위한 editdiary.html 파일을 만들어볼께. 경로는 src/main/resources/templates/diary/editdiary.html 이야.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Edit Diary</title>
</head>
<body>
<form th:action="@{/diary}" th:method="post">
<input type="hidden" name="_method" value="PUT" />
<input type="hidden" id="id" name="id" th:value="${diary.id}" />
<label for="diary_date">날짜:</label>
<input type="text" id="diary_date" name="diary_date" th:value="${#temporals.format(diary.diary_date, 'yyyy-MM-dd')}" />
<label for="diary_content">내용:</label>
<textarea rows="10" id="diary_content" name="diary_content" th:value="${diary.diary_content}" th:text="${diary.diary_content}"></textarea>
<input type="submit" value="저장(수정)" />
</form>
</body>
</html>
thymeleaf 문법이 사용된 부분을 굵은 글꼴로 표시해놨으니 참고하길 바랄께.
이제 이 html 파일을 호출하기 위한 컨트롤러를 만들어볼께. 기존의 DiaryController 는 @RestController 애노테이션을 붙였기 때문에 html 템플릿 파일을 호출하기에 부적절해. @RestController 애노테이션이 컨트롤러 함수가 리턴할 때 Rest API 방식으로 데이터를 리턴하도록 처리해주기 때문이지.
그래서 새로운 DiaryUIController 클래스를 만들고 @Controller 애노테이션을 붙여줄께.
package com.woohahaapps.study.diary.controller;
import com.woohahaapps.study.diary.domain.Diary;
import com.woohahaapps.study.diary.service.DiaryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
public class DiaryUIController {
private final DiaryService diaryService;
@Autowired
public DiaryUIController(DiaryService diaryService) {
this.diaryService = diaryService;
}
// Edit
@GetMapping("/diary/edit/{id}")
public String EditDiary(@PathVariable("id") Integer id, Model model) {
Diary diary = diaryService.GetDiary(id);
model.addAttribute("diary", diary);
return "diary/editdiary";
}
}
이제 웹브라우저에 http://localhost:8080/diary/edit/3 을 입력하면 id 가 3인 일기 데이터를 읽어서 템플릿 파일 diary/editdiary.html 파일을 호출하게 돼.
model.addAttribute(“diary”, diary); 가 diary 객체를 diary 라는 이름의 변수로 전달하는 코드야. 이렇게 되면 thymeleaf 문법의 코드에서 ${diary} 로 받아서 사용할 수가 있지.
실행된 모습을 볼까?
id 가 3인 일기 데이터가 화면에 표시가 되지. 내용을 수정해서 “저장(수정)” 버튼을 클릭하면 수정된 내용이 DB 에 저장되도록 코드를 짜면 되겠지.
editdiary.html 에서 action URL 로 /diary 를 설정했고, method 로는 PUT 을 설정했어(thymeleaf 에서 PUT method 를 이용해서 url 을 호출하도록 하기 위해서 <input type=”hidden” name=”_method” value=”PUT” /> 을 추가해주었어).
그리고 이 코드가 동작하게 하기 위해서 application.properties 또는 application.yml 에 아래와 같은 설정을 추가해 주어야 해.
application.properties
spring.mvc.hiddenmethod.filter.enabled=true
application.yml
mvc:
hiddenmethod:
filter:
enabled: true
이제 DiaryController 에 PUT method 에 대한 API 함수를 작성해볼께.
@RestController
public class DiaryController {
...
// Update
@PutMapping("/diary")
public void UpdateDiary(Integer id, String diary_date, String diary_content) {
System.out.println("id=" + id + ", date=" + diary_date + ", content=" + diary_content);
diaryService.UpdateDiary(id, diary_date, diary_content);
}
...
}
이어서 DiaryService 에도 UpdateDiary 함수를 작성해줘야지.
@Service
public class DiaryService {
...
public void UpdateDiary(Integer id, String diary_date, String diary_content) {
diaryMapper.UpdateDiary(id, diary_date, diary_content);
}
}
이번에는 DiaryMapper 인터페이스에 UpdateDiary 함수를 선언해볼께.
@Mapper
public interface DiaryMapper {
...
public void UpdateDiary(Integer id, String diary_date, String diary_content);
}
마지막으로 일기 데이터를 업데이트하는 쿼리문을 작성해볼 차례야.
<mapper namespace="com.woohahaapps.study.diary.mapper.DiaryMapper">
...
<update id="UpdateDiary">
update diary
set
diary_date = DATE(#{diary_date})
, diary_content = #{diary_content}
where
id = #{id};
</update>
</mapper>
이제 실행해서 확인을 한번 해볼까?
“저장(수정)” 버튼을 클릭하면 서버 로그에 아래와 같이 입력한 데이터가 표시돼.
데이터베이스를 조회해도 정상적으로 값이 저장된 것을 확인할 수가 있어.
DeleteDiary (Method:DELETE)
마지막으로 DeleteDiary 를 작성해볼께. id 값을 입력받아서 해당 id 에 해당하는 일기 데이터만 지워야 하니까 url 에 id 를 전달할 수 있도록 수정해야 해.
@RestController
public class DiaryController {
...
// Delete
@DeleteMapping("/diary/{id}")
public void DeleteDiary(@PathVariable("id") Integer id) {
diaryService.DeleteDiary(id);
}
}
서비스와 매퍼 인터페이스 DeleteDiary 함수를 추가할께.
DiaryService.java
@Service
public class DiaryService {
...
public void DeleteDiary(Integer id) {
diaryMapper.DeleteDiary(id);
}
}
DiaryMapper.java
@Mapper
public interface DiaryMapper {
...
public void DeleteDiary(Integer id);
}
마지막으로 매퍼 인터페이스의 DeleteDiary 함수 실행시 실행될 쿼리문을 작성해 주어야겠지.
<mapper namespace="com.woohahaapps.study.diary.mapper.DiaryMapper">
...
<delete id="DeleteDiary">
delete from diary
where
id = #{id};
</delete>
</mapper>
Postman 에서 DELETE method 를 테스트해볼께.
Send 버튼을 눌러서 실행시킨 후에 데이터베이스를 조회하면 id 가 3인 데이터가 삭제된 것을 확인할 수가 있어.
정리
지금까지 spring boot 에서 mybatis 를 활용해서 데이터베이스에 대한 액세스 프로그램 작성 과정을 살펴봤어. 가장 기본적인 코드 구조를 살펴본 것이기 때문에 더 수정해나갈 부분이 많은건 사실이지.
다음에 또 기회가 될 때마다 이 프로그램을 개선해 나가는 과정을 알아볼께.