Spring Boot์ MyBatis๋ฅผ ํ์ฉํ ์ฃผ๋ฌธ(Order) ๋ฐ ์ ํ(Product) ๊ด๋ฆฌ ์์คํ ์ ๋๋ค.
์ด ํ๋ก์ ํธ๋ Spring Boot 3.2.5์ MyBatis๋ฅผ ์ฌ์ฉํ์ฌ ์ฃผ๋ฌธ๊ณผ ์ ํ ์ ๋ณด๋ฅผ ๊ด๋ฆฌํ๋ ์น ์ ํ๋ฆฌ์ผ์ด์
์
๋๋ค.
1:N ๊ด๊ณ(์ฃผ๋ฌธ:์ ํ)๋ฅผ MyBatis์ collection ๋งคํ์ ํตํด ์ฒ๋ฆฌํ๋ฉฐ, ๋์ SQL์ ํ์ฉํ ์กฐ๊ฑด๋ถ ๊ฒ์ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
- Java: 22
- Spring Boot: 3.2.5
- MyBatis: 3.0.3
- Database: MySQL 5.1.45
- ํ ํ๋ฆฟ ์์ง: Thymeleaf
- ์ปค๋ฅ์ ํ: HikariCP
- ๋น๋ ๋๊ตฌ: Maven
- ํจํค์ง: WAR
- ๊ธฐํ: Lombok, log4jdbc-log4j2
src/
โโโ main/
โ โโโ java/
โ โ โโโ org/wrapper/mybatismapper/
โ โ โโโ configuration/
โ โ โ โโโ DatabaseConfiguration.java # MyBatis ๋ฐ ๋ฐ์ดํฐ์์ค ์ค์
โ โ โโโ MybatisMapperApplication.java # ๋ฉ์ธ ์ ํ๋ฆฌ์ผ์ด์
ํด๋์ค
โ โ โโโ order/
โ โ โ โโโ mapper/
โ โ โ โ โโโ OrderMapper.java # MyBatis ๋งคํผ ์ธํฐํ์ด์ค
โ โ โ โโโ service/
โ โ โ โ โโโ OrderService.java # ๋น์ฆ๋์ค ๋ก์ง ์๋น์ค
โ โ โ โโโ vo/
โ โ โ โโโ OrderVO.java # ์ฃผ๋ฌธ VO
โ โ โ โโโ ProductVO.java # ์ ํ VO
โ โ โ โโโ SearchVO.java # ๊ฒ์ ์กฐ๊ฑด VO
โ โ โโโ ServletInitializer.java # WAR ๋ฐฐํฌ์ฉ ์ด๊ธฐํ ํด๋์ค
โ โโโ resources/
โ โโโ application.properties # ์ ํ๋ฆฌ์ผ์ด์
์ค์
โ โโโ mybatis/
โ โ โโโ mapper/
โ โ โ โโโ OrderMap.xml # MyBatis ๋งคํผ XML
โ โ โโโ SQL(script)/
โ โ โโโ common.sql # ํ
์ด๋ธ ์์ฑ ์คํฌ๋ฆฝํธ
โ โ โโโ createUser.sql # ๋ฐ์ดํฐ๋ฒ ์ด์ค/์ฌ์ฉ์ ์์ฑ ์คํฌ๋ฆฝํธ
โ โ โโโ insert.sql # ์ํ ๋ฐ์ดํฐ ์ฝ์
์คํฌ๋ฆฝํธ
โ โโโ templates/
โ โโโ index.html # ๊ธฐ๋ณธ ํ์ด์ง
โโโ test/
โโโ java/
โโโ org/wrapper/mybatismapper/
โโโ order/
โโโ service/
โโโ OrderServiceTest.java # ์๋น์ค ํ
์คํธ ์ฝ๋
seq: ์ฃผ๋ฌธ ์ผ๋ จ๋ฒํธ (PK, AUTO_INCREMENT)userId: ํ์ ์์ด๋name: ํ์ ์ด๋ฆemail: ํ์ ์ด๋ฉ์ผphone: ํ์ ์ ํ๋ฒํธaddress: ์ฃผ์regDate: ์์ฑ์ผ์editDate: ์์ ์ผ์
seq: ์ ํ ์ผ๋ จ๋ฒํธ (PK)order_seq: ์ฃผ๋ฌธ ์ผ๋ จ๋ฒํธ (FK)name: ์ ํ ์ด๋ฆprice: ๊ฐ๊ฒฉregDate: ์์ฑ์ผ์editDate: ์์ ์ผ์
๊ด๊ณ: ํ๋์ ์ฃผ๋ฌธ(tb_order)์ ์ฌ๋ฌ ๊ฐ์ ์ ํ(tb_product)์ ๊ฐ์ง ์ ์์ต๋๋ค (1:N ๊ด๊ณ).
- Java 22 ์ด์
- Maven 3.6 ์ด์
- MySQL 5.7 ์ด์
-
์ ์ฅ์ ํด๋ก
git clone <repository-url> cd mybatis-mapper
-
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์
MySQL์ ์ ์ํ์ฌ ๋ค์ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํฉ๋๋ค:
-- createUser.sql ์คํ CREATE DATABASE orders DEFAULT CHARACTER SET UTF8; CREATE USER 'TP'@'localhost' IDENTIFIED BY '1234'; GRANT ALL PRIVILEGES ON orders.* TO 'TP'@'localhost'; FLUSH PRIVILEGES;
-
ํ ์ด๋ธ ์์ฑ ๋ฐ ์ํ ๋ฐ์ดํฐ ์ฝ์
orders๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ ํ:-- common.sql ์คํ (ํ ์ด๋ธ ์์ฑ) -- insert.sql ์คํ (์ํ ๋ฐ์ดํฐ ์ฝ์ )
-
๋ฐ์ดํฐ์์ค ์ค์
application.propertiesํ์ผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์ ๋ณด๋ฅผ ์ค์ ํฉ๋๋ค:spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost:3306/orders spring.datasource.hikari.username=TP spring.datasource.hikari.password=1234 spring.datasource.hikari.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
-
์ ํ๋ฆฌ์ผ์ด์ ์คํ
./mvnw spring-boot:run
๋๋ Maven์ด ์ค์น๋์ด ์๋ค๋ฉด:
mvn spring-boot:run
-
์ ํ๋ฆฌ์ผ์ด์ ์ ์
๋ธ๋ผ์ฐ์ ์์
http://localhost:8080์ ์
OrderMapper.selectOrderList(SearchVO searchVO) ๋ฉ์๋๋ฅผ ํตํด ๋ค์ํ ์กฐ๊ฑด์ผ๋ก ์ฃผ๋ฌธ ๋ชฉ๋ก์ ์กฐํํ ์ ์์ต๋๋ค.
์ง์ํ๋ ๊ฒ์ ์กฐ๊ฑด:
userId: ํ์ ์์ด๋ (LIKE ๊ฒ์)name: ํ์ ์ด๋ฆ (LIKE ๊ฒ์)email: ํ์ ์ด๋ฉ์ผ (์ ํํ ์ผ์น)address: ์ฃผ์ (์ ํํ ์ผ์น)productName: ์ ํ ์ด๋ฆ (์ ํํ ์ผ์น)price: ๊ฐ๊ฒฉ ์ดํ (์ดํ ๊ฒ์)
์์:
SearchVO searchVO = new SearchVO();
searchVO.setName("John");
searchVO.setPrice(90.0);
List<OrderVO> orders = orderService.selectOrderList(searchVO);OrderMapper.selectOrderListByOrderSeq(List<String> orderSeqList) ๋ฉ์๋๋ฅผ ํตํด ์ฌ๋ฌ ์ฃผ๋ฌธ ์ผ๋ จ๋ฒํธ๋ก ์ฃผ๋ฌธ ๋ชฉ๋ก์ ์กฐํํ ์ ์์ต๋๋ค.
์์:
List<String> orderSeqList = Arrays.asList("1", "2", "3");
List<OrderVO> orders = orderService.selectOrderListByOrderSeq(orderSeqList);MyBatis์ <collection> ํ๊ทธ๋ฅผ ์ฌ์ฉํ์ฌ ์ฃผ๋ฌธ(OrderVO)๊ณผ ์ ํ(ProductVO)์ 1:N ๊ด๊ณ๋ฅผ ์๋์ผ๋ก ๋งคํํฉ๋๋ค.
<resultMap id="OrderListMap" type="org.wrapper.mybatismapper.order.vo.OrderVO">
<id column="seq" property="seq" />
<result column="user_id" property="userId" />
<!-- ... -->
<collection property="productObjList" ofType="org.wrapper.mybatismapper.order.vo.ProductVO">
<id column="seq" property="seq" />
<result column="order_seq" property="orderSeq" />
<!-- ... -->
</collection>
</resultMap>ํ
์คํธ ์ฝ๋๋ OrderServiceTest ํด๋์ค์ ํฌํจ๋์ด ์์ต๋๋ค.
ํ ์คํธ ์คํ:
./mvnw test์ฃผ์ ํ ์คํธ ๋ฉ์๋:
selectOrderListWhereClause(): ์กฐ๊ฑด๋ถ ๊ฒ์ ํ ์คํธ (Deprecated)selectOrderListByOrderSeq(): ์ฃผ๋ฌธ ์ผ๋ จ๋ฒํธ ๋ฆฌ์คํธ๋ก ์กฐํ ํ ์คํธ
- @MapperScan:
org.wrapper.mybatismapper.**.mapperํจํค์ง์ ๋งคํผ ์ธํฐํ์ด์ค๋ฅผ ์๋ ์ค์บ - @EnableTransactionManagement: ํธ๋์ญ์ ๊ด๋ฆฌ ํ์ฑํ
- HikariCP: ๊ณ ์ฑ๋ฅ ์ปค๋ฅ์ ํ ์ฌ์ฉ
- SqlSessionFactory: MyBatis ์ธ์ ํฉํ ๋ฆฌ ์ค์
- Mapper XML ์์น:
classpath:/mybatis/mapper/*.xml
- @PostConstruct: ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ TimeZone์ UTC๋ก ์ค์
- WAR ํจํค์ง์ ์ํ
ServletInitializerํด๋์ค ํฌํจ
์ฃผ๋ฌธ ์ ๋ณด๋ฅผ ๋ด๋ Value Object์
๋๋ค. productObjList ํ๋๋ฅผ ํตํด ํด๋น ์ฃผ๋ฌธ์ ์ํ ์ ํ ๋ชฉ๋ก์ ํฌํจํฉ๋๋ค.
์ ํ ์ ๋ณด๋ฅผ ๋ด๋ Value Object์
๋๋ค. orderSeq ํ๋๋ก ์ฃผ๋ฌธ๊ณผ์ ๊ด๊ณ๋ฅผ ๋ํ๋
๋๋ค.
๊ฒ์ ์กฐ๊ฑด์ ๋ด๋ Value Object์ ๋๋ค. ๋ชจ๋ ํ๋๋ ์ ํ์ (Optional)์ด๋ฉฐ, ์ค์ ๋ ์กฐ๊ฑด๋ง WHERE ์ ์ ์ถ๊ฐ๋ฉ๋๋ค.
์ฃผ๋ฌธ ๊ด๋ จ ๋น์ฆ๋์ค ๋ก์ง์ ์ฒ๋ฆฌํ๋ ์๋น์ค ํด๋์ค์
๋๋ค. OrderMapper๋ฅผ ์ฃผ์
๋ฐ์ ์ฌ์ฉํฉ๋๋ค.
MyBatis์ ๋์ SQL ๊ธฐ๋ฅ์ ํ์ฉํ์ฌ ์กฐ๊ฑด๋ถ WHERE ์ ์ ์์ฑํฉ๋๋ค:
<sql id="selectListOrderWhereClause">
<if test="userId != null and userId != ''">
AND tb_o.user_id LIKE CONCAT(#{userId}, '%')
</if>
<!-- ... ๊ธฐํ ์กฐ๊ฑด๋ค ... -->
</sql>์ด ํ๋ก์ ํธ์๋ DB์ ์๋ฒ์ ์๊ฐ ๋ถ์ผ์น ๋ฌธ์ ๋ฅผ ํ์ธํ๊ณ ํ ์คํธํ ์ ์๋ ๊ธฐ๋ฅ์ด ํฌํจ๋์ด ์์ต๋๋ค.
ํฌ์คํ ์์ ์ธ๊ธํ ๋ด์ฉ์ ์ค์ ๋ก ํ์ธํ ์ ์๋ ํ ์คํธ ์ฝ๋์ API๋ฅผ ์ ๊ณตํฉ๋๋ค:
- DATETIME vs TIMESTAMP์ ํ์์กด ์ฒ๋ฆฌ ์ฐจ์ด
- LocalDateTime vs ZonedDateTime์ ๋์ ์ฐจ์ด
- JDBC URL์ serverTimezone ์ค์ ์ํฅ
- JVM ํ์์กด ์ค์ ์ํฅ
ํ์์กด ํ ์คํธ๋ฅผ ์ํ ํ ์ด๋ธ์ ์์ฑํฉ๋๋ค:
-- timezone_test.sql ์คํ
source src/main/resources/mybatis/SQL(script)/timezone_test.sqlGET /api/timezone/test-data
์๋ต ์์:
{
"jvmTimeZone": "Asia/Seoul",
"dbTimeZoneInfo": {
"global": "SYSTEM",
"session": "SYSTEM",
"system": "KST"
},
"data": [
{
"id": 1,
"zoneDateTime": "2024-09-09T15:25:51+09:00[Asia/Seoul]",
"localDateTime": "2024-09-09T15:25:51",
"zoneTimestamp": "2024-09-09T15:25:51+09:00[Asia/Seoul]",
"localTimestamp": "2024-09-09T15:25:51"
}
]
}GET /api/timezone/test-data/{id}
ํ์์กด ๋ณํ ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ํฌํจํ์ฌ ๋ฐํํฉ๋๋ค.
GET /api/timezone/db-timezone
POST /api/timezone/test-data/kst # KST ํ๊ฒฝ์์ ์์ฑ
POST /api/timezone/test-data/utc # UTC ํ๊ฒฝ์์ ์์ฑ
ํฌ์คํ ์์ ์ธ๊ธํ ์ฌ๋ฌ ์๋๋ฆฌ์ค๋ฅผ ํ ์คํธํ ์ ์์ต๋๋ค:
./mvnw test -Dtest=ZoneServiceTest์ฃผ์ ํ ์คํธ ์๋๋ฆฌ์ค:
-
ํ ์คํธ 1: ๊ธฐ๋ณธ ํ๊ฒฝ(KST)์์ ํ์์กด ๋์ ํ์ธ
- ์๋ฒ JVM ํ์์กด: KST
- JDBC URL ํ์์กด ์ค์ : ์์
- DB ํ์์กด: KST
-
ํ ์คํธ 2: JDBC URL UTC ์ค์ ์ ํ์์กด ๋์ ํ์ธ
- ์๋ฒ JVM ํ์์กด: KST
- JDBC URL ํ์์กด ์ค์ : UTC
- DB ํ์์กด: KST
-
์์ธ ๋ถ์: ํน์ ID์ ๋ฐ์ดํฐ๋ฅผ ์์ธ ๋ถ์ํ์ฌ ํ์์กด ๋ณํ ๋ฌธ์ ํ์ธ
-
ํ์์กด ๋ณํ ์ค๋ณต ๋ฌธ์ ์ฌํ: ํฌ์คํ ์์ ์ธ๊ธํ ๋ฌธ์ ์ ์ฌํ
-
application.yml ์ค์ ํ์ธ
spring: datasource: hikari: jdbc-url: jdbc:log4jdbc:mysql://localhost:3307/orders?serverTimezone=UTC&characterEncoding=UTF-8
serverTimezone=UTC์ค์ ์ฌ๋ถ์ ๋ฐ๋ผ ํ ์คํธ ๊ฒฐ๊ณผ๊ฐ ๋ฌ๋ผ์ง๋๋ค.
-
ํ ์คํธ ์คํ
- ํ ์คํธ ์ฝ๋๋ฅผ ์คํํ์ฌ ๋ก๊ทธ๋ฅผ ํ์ธํฉ๋๋ค.
- API๋ฅผ ํธ์ถํ์ฌ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํฉ๋๋ค.
-
๋ฌธ์ ํ์ธ
- ํฌ์คํ ์์ ์ธ๊ธํ ๋ฌธ์ ์ ๋ค์ด ์ค์ ๋ก ๋ฐ์ํ๋์ง ํ์ธํฉ๋๋ค.
- ZonedDateTime์ UTC๋ก ๋ณํํ ๋ ์์๊ณผ ๋ค๋ฅธ ๊ฒฐ๊ณผ๊ฐ ๋์ค๋์ง ํ์ธํฉ๋๋ค.
src/main/java/org/wrapper/mybatismapper/timezone/vo/ZoneEntity.javasrc/main/java/org/wrapper/mybatismapper/timezone/mapper/ZoneMapper.javasrc/main/resources/mybatis/mapper/ZoneMap.xmlsrc/main/java/org/wrapper/mybatismapper/timezone/service/ZoneService.javasrc/main/java/org/wrapper/mybatismapper/timezone/controller/ZoneController.javasrc/test/java/org/wrapper/mybatismapper/timezone/service/ZoneServiceTest.javasrc/main/resources/mybatis/SQL(script)/timezone_test.sql
- ์ด ํ๋ก์ ํธ๋ WAR ํ์ผ๋ก ํจํค์ง๋์ด ์ธ๋ถ ํฐ์บฃ ์๋ฒ์ ๋ฐฐํฌํ ์ ์์ต๋๋ค.
- SQL ๋ก๊น
์ ์ํด
log4jdbc-log4j2๋ฅผ ์ฌ์ฉํฉ๋๋ค. - TimeZone์ UTC๋ก ์ค์ ๋์ด ์์ต๋๋ค.
- Lombok์ ์ฌ์ฉํ์ฌ ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ฝ๋๋ฅผ ์ค์์ต๋๋ค.
- ํ์์กด ํ ์คํธ ๊ธฐ๋ฅ์ ํตํด DB์ ์๋ฒ์ ์๊ฐ ๋ถ์ผ์น ๋ฌธ์ ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
์ด ํ๋ก์ ํธ๋ ๊ฐ์ธ ํ์ต ๋ฐ ๊ต์ก ๋ชฉ์ ์ผ๋ก ์์ฑ๋์์ต๋๋ค.