본 포스팅의 예제는 STS(Spring Tool Studio) 또는 Eclipse를 사용하지 않고 intellij를 통해 구현하고 있습니다.
그래서 기존의 생성된 STS(Spring Tool Studio) 생성된 Spring 프로젝트의 스프링 설정 파일명과 프로젝트 구조가
약간 다를 수 있습니다. Intellij 스프링 mvc 프로젝트 생성 포스팅을 참고해주시면 감사하겠습니다.
SpringMVC + Mybatis DAO 구현 테스트
앞서 포스팅 한 Mybatis 설정 및 테스트에 이어서 Mybatis를 이용하여 회원을 입력하고, 조회(아이디, 아이디 + 비밀번호)
하는 간단한 기능을 구현해보고, 제대로 구현 되었는지 테스트까지 진행해 보겟습니다.
1. Mybatis의 개발 방식 정리
MyBatis는 JDBC에서 개발자가 직접 처리하는 PreparedStatement의 ?에 대한 설정이나 ResultSet을 이용한 처리가 이루어지기 떄문에 기존의 방식에 비해 생산이 좋습니다. MyBatis의 이전 버전은 iBatis는 개발자가 모든 SQL을 XML로 작성하고 SQL문을 사용하는 DAO클래스를 설계하여 SQL문을 호출하는 방식으로 작성 되었지만, MybBatis의 경우 어노테이션을 지원하고, 인터페이스와 어노테이션을 통해 SQL문을 설정하고 처리 할 수 있게 되었습니다.
MyBatis를 이용할 떄 SQL문을 사용하는 방식의 종류는 다음과 같습니다.
방식 | 장정 | 단점 |
XML만을 사용하는 방식 | SQL문은 별도의 XML로 작성되기 떄문에 SQL문의 수정이나 유지 보수에 용이 하다는 장점이 있습니다. | 개발시 코드의 양이 많아지고, 코드의 복잡성이 증가 하는 단점이 있습니다. |
어노테이션과 인터페이스만을 이용하는 방식 | 별도의 DAO없이도 개발이 가능하기 때문에 생산성이 증가 합니다. | SQL문을 어노테이션으로 작성하기 떄문에 매번 수정이 일어나는 경우 다시 컴파일 해야 되는 번거로운 단점이 있습니다. |
인터페이스와 XML로 작서된 SQL문을 활용하는 방식 | 간단한 SQL문은 어노테이션으로 복잡한 SQL문은 xml로 처리 하므로 상황에 따라 유연하게 처리 할 수 있다는 장점이 있습니다. | 개발자에 따라 개발 방식의 차이가 존재하기 떄문에 유지보수에 불편함이 존재하는 단점이 있습니다. |
2. XML만을 이용하는 방식 구현하기
앞서 언급한 것처럼 XML만을 이용하는 방식은 SQL문을 완전히 분리해서 처리하기 때문에 SQL문의 변경이 일어날 떄, 대처가 수월해 유지보수에 적합하다는 장점을 가지고 있습니다. 그래서 대부분의 프로젝트에서 XML만을 이용해서 SQL문을 작성하고, 별도의 DAO를 만드는 방식을 선호 합니다.
이제 본격적으로 xml만을 이용하는 방식을 통해 간단한 회원가입 로직을 구현 해보겠습니다.
# DB(MariaDB) 테이블 작성
기존에 앞서 작성한 schema eden에 회원 테이블을 추가 시켜 주세요.
create table member ( userid varchar(50) not null, -- 회원아이디 userpw varchar(50) not null, -- 회원비밀번호 username varchar(50) not null, -- 회원이름 email varchar(50) not null, -- 회원이메일 regdate timestamp default now(), -- 가입일자 updatedate timestamp default now(), -- 수정일자 primary key (userid) -- ㅣ본키 : userid ); | cs |
# 도메인 객체를 위한 클래스 설계
domain 패키지를 생성하고, 회원을 의미하는 UserVo클래스를 아래와 같이 작성 해주세요
package com.eden.sample.domain; import java.util.Date; public class UserVo { private String userid; // 회원아이디 private String userpw; // 회원비밀번호 private String username; // 회원이름 private String email; // 회원이메일 private Date regdate; // 가입일자 private Date updatedate; // 수정일자 public String getUserid() { return userid; } public void setUserid(String userid) { this.userid = userid; } public String getUserpw() { return userpw; } public void setUserpw(String userpw) { this.userpw = userpw; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public Date getRegdate() { return regdate; } public void setRegdate(Date regdate) { this.regdate = regdate; } public Date getUpdatedate() { return updatedate; } public void setUpdatedate(Date updatedate) { this.updatedate = updatedate; } @Override public String toString() { return "UserVo{" + "userid='" + userid + '\'' + ", userpw='" + userpw + '\'' + ", username='" + username + '\'' + ", email='" + email + '\'' + ", regdate=" + regdate + ", updatedate=" + updatedate + '}'; } } | cs |
# DAO(Data Acces Object) 인터페이스 작성
persitence패키지를 생성하고 UserDAO 인터페이스를 생성하고, 간단한 회원관련 기능을 처리할 메서드를 작성해주세요.
package com.eden.sample.persistence; import com.eden.sample.domain.UserVo; public interface UserDAO { // 현재시간 체크 public String getTime(); // 회원 입력 public void insertUser(UserVo userVO); // 회원 아이디로 조회 public UserVo readUser(String userid); // 회원아이디, 비밀번호로 조회 public UserVo readWithPW(String userid, String userpw); } | cs |
# XML Mapper 작성
src/main/resources/mapper/tutorial 디렉토리와 userMapper.xml파일을 생성하고 아래와 같이 mapper를 작성해주세요
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTO Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.eden.sample.mappers.tutorial.UserMapper"> <select id="getTime" resultType="string"> select now() </select> <insert id="insertUser"> insert into member (userid, userpw, username, email) values (#{userid}, #{userpw}, #{username}, #{email}) </insert> <select id="selectUser" resultType="com.eden.sample.domain.UserVo"> select * from member where userid = #{userid} </select> <select id="readWithPW" resultType="com.eden.sample.domain.UserVo"> select * from member where userid = #{userid} and userpw = #{userpw} </select> </mapper> | cs |
여기서 가장 주의해야할 것은 mapper 태그의 namespace속성에 가장 신경을 써주셔야 합니다. 클래스의 패키지와 유사한 용도로 Mybatis내에서 원하는 SQL문을 찾아서 실행할 떄 동작하기 떄문입니다.
# myBatis-Spring에서 XML Mapper 인식시키기
MyBatis가 XML Mapper를 인식할 수 있도록 applicationContext.xml에 아래와 같이 Mapper의 경로를 추개해주세요. 여기서 applicationContext.xml 은 sts에서의 root-context.xml 파일과 동일한 역할을 수행하고 있습니다.
<!-- SqlSessionFactory 설정 : dataSource를 참조, mybatis-config.xml 경로 설정 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:/mybatis-config.xml"/> <!-- mapper 경로 추가 --> <property name="mapperLocations" value="classpath:/mappers/**/*Mapper.xml"/> </bean> | cs |
# sqlSessionTemplate 설정
MyBatis에서 DAO를 이용하는 경우 SqlSessionTemplate을 이용해 DAO를 구현하기 떄문에 DAO 인터페이스를 구현하기에 앞서 SqlSessionTemplate 를 먼저 설정을 해주셔야 합니다. DAO 작업에서 가장 번거로운 작업은ㅇ 데이터베이스와 연결을 맺고, 작업이 완료된 후 연결을 close() 하는 작업인데 mybatis-spring라이브러리는 이것을 처리할 수 있는 SqlSessionTemplate이라는 클래스를 통해 DB를 연결하고, 종료하는 작업을 처리 할 수 있습니다. SqlSessionTemplate은 SqlSession 인터페이를 구현한 클래스로 기본적인 트랜잭션의 관리나 쓰레드 처리의 안정성을 보장해주고, DB의 연결과 종료를 책임 집니다.
SqlSessionTemplate설정은 applicationContext.xml에서 SqlSessionFactory를 생성자로 주입해서 아래와 같이 설정해주시면 됩니다.
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate" destroy-method="clearCache"> <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"/> </bean> | cs |
# DAO인터페이스 구현 클래스 작성
앞서 작성한 UserDAO인터페이스를 구현하는 클래스를 작성하고, SqlSessionTemplate을 주입받아 사용합니다.
/* 작성일시 : 2023-02-13 작성자 : Eden 작성시간 : 오전 11:05 용도 : */ package com.eden.sample.persistence; import com.eden.sample.domain.UserVo; import org.apache.ibatis.session.SqlSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import java.util.HashMap; @Repository public class UserDAOImpl implements UserDAO { @Autowired SqlSession sqlSession; private static final String NAMESPACE = "com.eden.sample.mappers.tutorial.userMapper"; @Override public String getTime() { return sqlSession.selectOne(NAMESPACE + ".getTime"); } @Override public void insertUser(UserVo userVO) { sqlSession.insert(NAMESPACE + ".insertUser", userVO); } @Override public UserVo readUser(String userid) { return sqlSession.selectOne(NAMESPACE + ".selectUser", userid); } @Override public UserVo readWithPW(String userid, String userpw) { HashMap<String, Object> map = new HashMap<>(); map.put("userid", userid); map.put("userpw", userpw); return sqlSession.selectOne(NAMESPACE + ".readWithPW", map); } } | cs |
# Spring에서 Bean등록
UserDAOImpl 에 @Repository 어노테이션 설정을 했더라고, 스프링에서 해당 패키지를 스캔하지 않으면 스프링의 빈으로 등록되지 못하기 떄문에 아래와 같이 applicationContext.xml에 작성을 반드시 해주셔야 합니다.
<context:component-scan base-package="com.eden.sample" /> | cs |
# MyBatis 로그 (log4jdbc-log4j2)
MyBatis을 사용해 개발을 하다보면 잘못된 SQL이나 잘못된 속성명으로 인해 예외가 발생하는 경우가 생기는데 이러한 경우를 대비해 MyBatis의 로그를 보다 자세히 조사할 수 있도록 설정해주는 것이 좋습니다. 보다 자세한 로그를 보기 위해서는 log4jdbc-log4j2 라이브러리를 추가해주세요.
<dependency> <groupId>org.bgee.log4jdbc-log4j2</groupId> <artifactId>log4jdbc-log4j2-jdbc4</artifactId> <version>1.16</version> </dependency> | cs |
그리고 나서 applicationContext.xml에서 약간의 수정이 필요합니다. dataSource 설정에서 driverClassName과 url 의 value 를 아래와 같이 변경해주세요
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy"/> <property name="url" value="jdbc:log4jdbc:mariadb://127.0.0.1:3306/eden"/> <property name="username" value="아이디"/> <property name="password" value="패스워드"/> </bean> | cs |
log4jdbc-log4j2가 제대로 동작하기 위해서는 별도의 로그관련 설정이 추가적으로 필요합니다.
src/main/resources의 디렉토리에 log4jdbc.log4j2.properties와 logback.xml 파일을 생성해주고 아래와 같이 코드를 작성해주세요.
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator | cs |
<?xml version="1.0" encoding="UTF-8"?> <configuration> <include resource="org/springframework/boot/logging/logback/base.xml"/> <!-- log4jdbc-log4j2 --> <logger name="jdbc.sqlonly" level="DEBUG"/> <logger name="jdbc.sqltiming" level="INFO"/> <logger name="jdbc.audit" level="WARN"/> <logger name="jdbc.resultset" level="ERROR"/> <logger name="jdbc.resultsettable" level="ERROR"/> <logger name="jdbc.connection" level="INFO"/> </configuration> | cs |
마지막으로 src/main/resources/ 디렉토리의 log4j.xml를 src/test/resources 디렉토리에 동일하게 파일을 생성하고 아래와 같이 작성해줘야 로그가 제대로 출력됩니다.
# 테스트 코드 작성하기
이제 UserDAOImpl에서 구현한 기능들이 제대로 동작하는지 확인하기 위해 테스트 코드를 작성하도록 하겠습니다.
package com.eden.sample; import com.eden.sample.domain.UserVo; import com.eden.sample.persistence.UserDAO; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/applicationContext.xml"}) public class UserDAOTest { @Autowired private UserDAO userDAO; // 현재시간 출력 테스트 @Test public void testTime() { System.out.println(userDAO.getTime()); } // 회원 가입 테스트 @Test public void testInsertUser() { UserVo userVO = new UserVo(); userVO.setUserid("user00"); userVO.setUserpw("user00"); userVO.setUsername("user00"); userVO.setEmail("user00@mail.com"); userDAO.insertUser(userVO); } // 회원 조회 테스트 1: 아이디만 @Test public void testReadUser() { userDAO.readUser("user00"); } // 회원 조회 테스트 2: 아이디 + 비밀번호 @Test public void testReadWithPW() { userDAO.readWithPW("user00", "user00"); } } | cs |
이제 테스트를 돌려보면 아래와 같이 각각의 메서드의 테스트 결과가 콘솔창에 깔끔하게 로그가 출력되는 것을 볼 수 있습니다.
INFO : org.springframework.test.context.support.DefaultTestContextBootstrapper - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener, org.springframework.test.context.event.EventPublishingTestExecutionListener] INFO : org.springframework.test.context.support.DefaultTestContextBootstrapper - Using TestExecutionListeners: [org.springframework.test.context.web.ServletTestExecutionListener@10d68fcd, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@117e949d, org.springframework.test.context.support.DependencyInjectionTestExecutionListener@6db9f5a4, org.springframework.test.context.support.DirtiesContextTestExecutionListener@5f8edcc5, org.springframework.test.context.transaction.TransactionalTestExecutionListener@7b02881e, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener@1ebd319f, org.springframework.test.context.event.EventPublishingTestExecutionListener@3c0be339] INFO : jdbc.connection - 1. Connection opened INFO : jdbc.audit - 1. Connection.new Connection returned INFO : jdbc.audit - 1. Connection.getAutoCommit() returned true INFO : jdbc.audit - 1. PreparedStatement.new PreparedStatement returned INFO : jdbc.audit - 1. Connection.prepareStatement(select now()) returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@622ef26a INFO : jdbc.sqlonly - select now() INFO : jdbc.sqltiming - select now() {executed in 24 msec} INFO : jdbc.audit - 1. PreparedStatement.execute() returned true INFO : jdbc.resultset - 1. ResultSet.new ResultSet returned INFO : jdbc.audit - 1. PreparedStatement.getResultSet() returned net.sf.log4jdbc.sql.jdbcapi.ResultSetSpy@64b31700 INFO : jdbc.resultset - 1. ResultSet.getMetaData() returned org.mariadb.jdbc.client.result.ResultSetMetaData@214894fc INFO : jdbc.resultset - 1. ResultSet.getType() returned 1003 INFO : jdbc.resultset - 1. ResultSet.next() returned true WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by org.apache.ibatis.reflection.Reflector (file:/C:/Users/EdenDEV/.m2/repository/org/mybatis/mybatis/3.4.6/mybatis-3.4.6.jar) to method java.lang.String.value() WARNING: Please consider reporting this to the maintainers of org.apache.ibatis.reflection.Reflector WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release INFO : jdbc.resultset - 1. ResultSet.getString(now()) returned 2023-02-13 11:56:58 INFO : jdbc.resultset - 1. ResultSet.wasNull() returned false INFO : jdbc.resultsettable - |--------------------| |now() | |--------------------| |2023-02-13 11:56:58 | |--------------------| INFO : jdbc.resultset - 1. ResultSet.next() returned false INFO : jdbc.resultset - 1. ResultSet.close() returned void INFO : jdbc.audit - 1. PreparedStatement.getConnection() returned net.sf.log4jdbc.sql.jdbcapi.ConnectionSpy@6c44052e INFO : jdbc.audit - 1. Connection.getMetaData() returned org.mariadb.jdbc.DatabaseMetaData@36b6964d INFO : jdbc.audit - 1. PreparedStatement.getMoreResults() returned false INFO : jdbc.audit - 1. PreparedStatement.getUpdateCount() returned -1 INFO : jdbc.audit - 1. PreparedStatement.isClosed() returned false INFO : jdbc.audit - 1. PreparedStatement.close() returned INFO : jdbc.connection - 1. Connection closed INFO : jdbc.audit - 1. Connection.close() returned 2023-02-13 11:56:58 INFO : jdbc.connection - 2. Connection opened INFO : jdbc.audit - 2. Connection.new Connection returned INFO : jdbc.audit - 2. Connection.getAutoCommit() returned true INFO : jdbc.audit - 2. PreparedStatement.new PreparedStatement returned INFO : jdbc.audit - 2. Connection.prepareStatement(select * from member where userid = ? and userpw = ?) returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@50cf5a23 INFO : jdbc.audit - 2. PreparedStatement.setString(1, "user00") returned INFO : jdbc.audit - 2. PreparedStatement.setString(2, "user00") returned INFO : jdbc.sqlonly - select * from member where userid = 'user00' and userpw = 'user00' INFO : jdbc.sqltiming - select * from member where userid = 'user00' and userpw = 'user00' {executed in 207 msec} INFO : jdbc.audit - 2. PreparedStatement.execute() returned true INFO : jdbc.resultset - 2. ResultSet.new ResultSet returned INFO : jdbc.audit - 2. PreparedStatement.getResultSet() returned net.sf.log4jdbc.sql.jdbcapi.ResultSetSpy@2899a8db INFO : jdbc.resultset - 2. ResultSet.getMetaData() returned org.mariadb.jdbc.client.result.ResultSetMetaData@1e8823d2 INFO : jdbc.resultset - 2. ResultSet.getType() returned 1003 INFO : jdbc.resultsettable - |-------|-------|---------|------|--------|-----------| |userid |userpw |username |email |regdate |updatedate | |-------|-------|---------|------|--------|-----------| |-------|-------|---------|------|--------|-----------| INFO : jdbc.resultset - 2. ResultSet.next() returned false INFO : jdbc.resultset - 2. ResultSet.close() returned void INFO : jdbc.audit - 2. PreparedStatement.getConnection() returned net.sf.log4jdbc.sql.jdbcapi.ConnectionSpy@4c432866 INFO : jdbc.audit - 2. Connection.getMetaData() returned org.mariadb.jdbc.DatabaseMetaData@12365c88 INFO : jdbc.audit - 2. PreparedStatement.getMoreResults() returned false INFO : jdbc.audit - 2. PreparedStatement.getUpdateCount() returned -1 INFO : jdbc.audit - 2. PreparedStatement.isClosed() returned false INFO : jdbc.audit - 2. PreparedStatement.close() returned INFO : jdbc.connection - 2. Connection closed INFO : jdbc.audit - 2. Connection.close() returned INFO : jdbc.connection - 3. Connection opened INFO : jdbc.audit - 3. Connection.new Connection returned INFO : jdbc.audit - 3. Connection.getAutoCommit() returned true INFO : jdbc.audit - 3. PreparedStatement.new PreparedStatement returned INFO : jdbc.audit - 3. Connection.prepareStatement(select * from member where userid = ?) returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@64d7b720 INFO : jdbc.audit - 3. PreparedStatement.setString(1, "user00") returned INFO : jdbc.sqlonly - select * from member where userid = 'user00' INFO : jdbc.sqltiming - select * from member where userid = 'user00' {executed in 4 msec} INFO : jdbc.audit - 3. PreparedStatement.execute() returned true INFO : jdbc.resultset - 3. ResultSet.new ResultSet returned INFO : jdbc.audit - 3. PreparedStatement.getResultSet() returned net.sf.log4jdbc.sql.jdbcapi.ResultSetSpy@30272916 INFO : jdbc.resultset - 3. ResultSet.getMetaData() returned org.mariadb.jdbc.client.result.ResultSetMetaData@5bb3d42d INFO : jdbc.resultset - 3. ResultSet.getType() returned 1003 INFO : jdbc.resultsettable - |-------|-------|---------|------|--------|-----------| |userid |userpw |username |email |regdate |updatedate | |-------|-------|---------|------|--------|-----------| |-------|-------|---------|------|--------|-----------| INFO : jdbc.resultset - 3. ResultSet.next() returned false INFO : jdbc.resultset - 3. ResultSet.close() returned void INFO : jdbc.audit - 3. PreparedStatement.getConnection() returned net.sf.log4jdbc.sql.jdbcapi.ConnectionSpy@5bf61e67 INFO : jdbc.audit - 3. Connection.getMetaData() returned org.mariadb.jdbc.DatabaseMetaData@2c1dc8e INFO : jdbc.audit - 3. PreparedStatement.getMoreResults() returned false INFO : jdbc.audit - 3. PreparedStatement.getUpdateCount() returned -1 INFO : jdbc.audit - 3. PreparedStatement.isClosed() returned false INFO : jdbc.audit - 3. PreparedStatement.close() returned INFO : jdbc.connection - 3. Connection closed INFO : jdbc.audit - 3. Connection.close() returned INFO : jdbc.connection - 4. Connection opened INFO : jdbc.audit - 4. Connection.new Connection returned INFO : jdbc.audit - 4. Connection.getAutoCommit() returned true INFO : jdbc.audit - 4. PreparedStatement.new PreparedStatement returned INFO : jdbc.audit - 4. Connection.prepareStatement(insert into member (userid, userpw, username, email) values (?, ?, ?, ?)) returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@15051a0 INFO : jdbc.audit - 4. PreparedStatement.setString(1, "user00") returned INFO : jdbc.audit - 4. PreparedStatement.setString(2, "user00") returned INFO : jdbc.audit - 4. PreparedStatement.setString(3, "user00") returned INFO : jdbc.audit - 4. PreparedStatement.setString(4, "user00@mail.com") returned INFO : jdbc.sqlonly - insert into member (userid, userpw, username, email) values ('user00', 'user00', 'user00', 'user00@mail.com') INFO : jdbc.sqltiming - insert into member (userid, userpw, username, email) values ('user00', 'user00', 'user00', 'user00@mail.com') {executed in 15 msec} INFO : jdbc.audit - 4. PreparedStatement.execute() returned false INFO : jdbc.audit - 4. PreparedStatement.getUpdateCount() returned 1 INFO : jdbc.audit - 4. PreparedStatement.isClosed() returned false INFO : jdbc.audit - 4. PreparedStatement.close() returned INFO : jdbc.connection - 4. Connection closed INFO : jdbc.audit - 4. Connection.close() returned | cs |
'스프링 프레임워크 > 스프링 기본 개념 정리 및 기본 예제' 카테고리의 다른 글
Spring MVC Interceptor (0) | 2023.02.24 |
---|---|
Spring MVC - Controller 작성 연습 (0) | 2023.02.13 |
Spring MVC 구조 (0) | 2023.02.09 |
Spring MVC - Mybatis 설정 및 테스트 (0) | 2023.02.09 |
Spring MVC - MariaDB 연결 테스트 (0) | 2023.02.08 |
댓글