/*
*
* 자바웹개발워크북의 내용을 정리하기 위한 포스팅입니다.
* 스프링, 의존성주입, MyBatis연동
*/
1. 스프링의 시작 :
스프링프레임워크는 원래 웹이라는 제한적인 용도로만 쓰이는 것이 아니라 객체지향의 '의존성주입(dependency injection)'기법을 적용할 수 있는 객체지향 프레임워크였습니다. 스프링프레임워크는 로드존슨이 2002년도에 집필했던 'J2EE설계 및 개발'이라는 책의 예제코드에서 시작됐는데 말그대로 효과적이고 가볍게 J2EE를 이용할 수 있다는 것을 증명하면서 예제의 코드를 발전시킨것입니다.
2000년당시 자바진영에서 javaEE의 여러가지 스펙을 정의하고 비대해지는 동안, 스프링프레임워크는 반대로 경량프레임워크를 목표했습니다. 스프링프레임워크는 가장 중요한 '코어'역할을 하는 라이브러리와 여러개의 추가적인 라이브러리를 결합하는 형태로 프로젝트를 구성하는데 가장 대표적으로 웹MVC 구현을 쉽게할 수 있는 'Spring Web MVC'나 'JDBC처리를 쉽게할 수 있는 'Mybatis'를 연동하는 'mybatis-spring'과 같은 라이브러리가 그러한 예입니다.
2. 의존성주입 :
의존성주입은 어떻게하면 '객체와 객체간의 관계를 더 유연하게 유지할 것인가?'에 대한 고민으로 객체의 생성과 관계를 효과적으로 분리할 수 있는 방법에대한 고민입니다.
예를들어 모든 컨트롤러들은 TodoService나 MemberService같은 서비스 객체를 이용해야만 합니다. 이 경우 컨트롤러는 서비스객체에 의존적이라고 표현합니다. 즉 의존성이란 하나의 객체가 자신이 해야하는 일을 하기 위해 다른 객체의 도움이 필수적인 관계를 의미합니다.
과거에는 의존성을 해결하기 위해 컨트롤러에서 직접 서비스 개체를 생성하거나 하나의 객체만을 생성해서 활용하는 등의 다양한 패턴을 설계해서 적용해 왔는데 스프링프레임워크는 바로 이런점을 프레임워크 자체에서 지원하고 있습니다.
스프링프레임워크는 다양한 방식으로 필요한 객체를 찾아서 사용할 수 있도록 XML설정이나 자바설정등을 이용할 수 있습니다.
3. 스프링라이브러리추가
프레임워크 관련 라이브러리 버전은 구글을 이용해서 메이븐저장소를 찾을 수 있습니다.
가장 먼저 'Spring Core'라는 라이브러리를 찾습니다. 여러 라이브러리를 추가할 때는 같은 버전을 이용하도록 주의합니다.
build.gradle파일에 조장합니다. 다음 스프링과 관련된 'spring context', 'spring test'라이브러리를 추가합니다.
dependencies {
compileOnly('javax.servlet:javax.servlet-api:4.0.1')
testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")
// https://mvnrepository.com/artifact/org.springframework/spring-core
implementation 'org.springframework:spring-core:5.3.19'
// https://mvnrepository.com/artifact/org.springframework/spring-context
implementation 'org.springframework:spring-context:5.3.19'
// https://mvnrepository.com/artifact/org.springframework/spring-test
testImplementation 'org.springframework:spring-test:5.3.19'
}
Lombok라이브러리 추가합니다. 테스트환경에서도 사용할 수 있도록 테스트관련 설정도 한번에 추가합니다.
// https://mvnrepository.com/artifact/org.projectlombok/lombok
compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
Log4j2라이브러리를 추가합니다.
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
implementation 'org.apache.logging.log4j:log4j-core:2.17.2'
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api
implementation 'org.apache.logging.log4j:log4j-api:2.17.2'
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl
testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.17.2'
라이브러리를 추가한 후 resource폴더에 log4j2.xml을 추가하고 아래코드를 넣습니다.
<?xml version="1.0" encoding="UTF-8" ?>
<configuration status="INFO">
<Appenders>
<!-- 콘솔 -->
<Console name="console" target="SYSTEM_OUT">
<PatternLayout charset="UTF-8" pattern="%d{hh:mm:ss} %5p [%c] %m%n"/>
</Console>
</Appenders>
<loggers>
<logger name="org.springframework" level="INFO" additivity="false">
<appender-ref ref="console"/>
</logger>
<logger name="org.zerock" level="INFO" additivity="false">
<appender-ref ref="console"/>
</logger>
<root level="INFO" additivity="false">
<AppenderRef ref="console"/>
</root>
</loggers>
</configuration>
JSTL 라이브러리를 추가합니다.
// https://mvnrepository.com/artifact/javax.servlet.jsp.jstl/jstl
implementation 'javax.servlet.jsp.jstl:jstl:1.2'
4. 의존성 주입하기
스프링 프레임워크는 자체적으로 객체를 생성하고 관리하면서 필요한 곳으로 객체를 주입(inject)하는 역할을 하는데,
이를 위해서는 설정파일이나 어노테이션 등을 이용해야 합니다.
스프링이 관리하는 객체들은 빈(Bean)이라는 이름으로 불리는데 프로젝트 내에서 어떤 빈들을 어떻게 관리할 것인지를 설정하는 설정파일을 작성할 수 있습니다.
스프링의 빈 설정은 XML을 이용하거나 별도의 클래스를 이용하는 자바 설정이 가능합니다.
설정파일 추가 :
프로젝트의 WEB-INF폴더에서 [New > XML Configuration File > Spring Config]
파일의 이름은 'root-context.xml'으로 지정합니다. 해당 파일을 열면 오른쪽 상단에 [Configure application context]라는 설정메뉴가 보이는데 이는 현재 프로젝트를 인텔리제이에서 스프링 프레임워크로 인식하고 필요한 기능들을 지원하기 위한 설정입니다.
[Create new application context...] 항목을 선택하고 [root-context.xml]을 선택합니다.
root-context.xml의 내부에 <bean>이라는 태그를 이용해서 SampleService, SampleDAO를 다음과 같이 설정합니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.zerock.springex.sample.SapleDAO"></bean>
<bean class="org.zerock.springex.sample.SampleService"></bean>
</beans>
스프링의 빈 설정 테스트 :
스프링으로 프로젝트를 구성하는 경우 상당히 많은 객체를 설정하기 때문에 나중에 에러가 나면 원인을 찾기 어렵습니다.
따라서 가능하다면 개발 단계에서 많은 테스트를 진행하면서 개발하는 것이 좋습니다.
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class SampleTests {
@Autowired
private SampleService sampleService;
@Test
public void testService1() {
log.info(sampleService);
Assertions.assertNotNull(sampleService);
}
}
@ExtendWith(SpringExtension.class) : JUnit5버전에서 'spring-test'를 이용하기 위한 설정입니다.(JUnit4 : @RunWith)
@ContextConfiguration: 스프링의 설정 정보를 로딩하기 위해서 사용합니다. 현재 프로젝트의 경우 XML로 설정돼있기 때문에 location속성을 이용하고, 자바걸정을 이용하는 경우에는 classes속성을 이용합니다.
@Aurowired : 스프링에서 사용하는 의존성 주입 관련 어노테이션으로 '만일 해당 타입의 빈이 존재하면 여기에 주입해 주기를 원한다'라는 의미입니다.
만일 root-context.xml에 SampleService에 대한 설정없이 테스트를 실행해보면 에러가 발생합니다.
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.zerock.springex.sample.SampleTests': Unsatisfied dependency expressed through field 'sampleService'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.zerock.springex.sample.SampleService' available: expected at least 1 bean which qualifies as autowire candidate.
에러의 내용은 SampleService타입의 객체를 주입하려고 하지만 해당 타입의 객체가 스프링 내에 등록된것이 없다고 합니다.
ApplicationContext와 빈(Bean) :
웹을 공부할때 서블릿이 존재하는 공간을 서블릿컨텍스트라고 했던것처럼, 스프링에서는 빈이라고 부르는 객체들을 관리하기 위해서 ApplicationContext라는 존재를 활용합니다.
예제의 경우 ApplicationContext는 root-context.xml을 이용해서 스프링이 실행되고 ApplicationContext객체가 생성됩니다.
root-context.xml을 읽으면 SampleService, SampleDAO가 <bean>으로 지정되어 있기 때문에 해당 클래스의 객체를 생성해서 관리하기 시작합니다.
@Autowired의 의미와 필드주입 :
테스트를 실행하면 @Autowired가 처리된 부분에 맞는 타입의 빈이 존재하는지를 확인하고 이를 테스트 코드 실행 시에 주입하게 됩니다.
멤버변수에 직접 @Autowired를 선언하는 방식을 '필드주입(Field Injection)'방식이라고 합니다.
<context:component-scan> :
스프링을 이용할때는 클래스를 작성하거나 객체를 직접 생성하지 않습니다. 이 역할은 스프링내부에서 이루어지며 ApplicationContext가 생성된 객체들을 관리하게 됩니다.
개발자가 직접 객체를 생성하지 않는 방식이 서블릿과 유사합니다.
서블릿을 생성하면 톰캣이 웹애플리케이션을 실행하고 필요할때 서블릿객체를 만드는 방식처럼...
과거에는 서블릿기술도 web.xml에 <servlet>태크를 이용해서 서블릿 클래스의 이름과 경로를 전부 기록했지만, 최근엔 @WebServlet 어노테이션이 이를 대신하고 있습니다.
스프링도 비슷한 방식으로 발전해왔습니다. 초기엔 XML파일에 <bean>이라는것을 이용해서 설정하는 방식이 2.5버전이후에 어노테이션 형태로 변화되면서 편리한 설정이 가능해졌습니다.
5. 스프링프레임워크 어노테이션
■ @Controller : MVC의 컨트롤러를 위한 어노테이션
■ @Service : 서비스 계층의 객체를 위한 어노테이션
■ @Repository : DAO와 같은 객체를 위한 어노테이션
■ @Component : 일반 객체나 유틸리티 객체를 위한 어노테이션
root-context.xml의 설정을 다음과 같이 변경합니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.zerock.springex.sample"/>
</beans>
component-scan이 추가됐는데 속성값으로는 패키지를 지정하게 됩니다.
'component-scan'은 해당 패키지를 스캔해서 스프링의 어노테이션들을 인식합니다.
6. 생성자 주입방식
초기 스프링에서는 @Autowired를 멤버변수에 할당하거나, Setter를 작성하는 방식을 많이 이용해 왔지만,
스프링 3 이후에는 생성자 주입방식이라고 부르는 방식을 더 많이 활용하고 있습니다.
생성자 주입방식 :
■ 주입 받아야 하는 객체의 변수는 final로 작성합니다.
■ 생성자를 이용해서 해당 변수를 생성자의 파라미터로 지정합니다.
생성자 주입 방식은 객체를 생성할 때 문제가 발생하는지를 미리 확인할 수 있기 때문에 '필드주입'이나 'Setter주입'보다 선호됩니다.
Lombok에서는 @RequiredArgsConstructor를 이용해서 간단하게 필요한 생성자를 자동으로 작성할수 있습니다.
@Service
@ToString
public class SampleService {
@Autowired
private SampleDAO sampleDAO;
}
↓
@Service
@ToString
@RequiredArgsConstructor
public class SampleService {
private final SampleDAO sampleDAO;
}
7. 인터페이스를 이용한 느슨한 결합
스프링이 의존성 주입을 가능하게 하지만 근본적으로 유연한 프로그램을 설계하기 위해서는 인터페이스를 이용해서,
나중에 다른 클래스의 객체로 쉽게 변경할 수 있도록 하는 것이 좋습니다.
예를들어 SampleDAO를 다른 객체로 변경하려면 결론적으로 SampleService코드 역시 수정되어야만 합니다.
추상화된 타입을 이용하면 이러한 문제를 피할수 있는데 대표적인게 인터페이스입니다.
인터페이스를 이용하면 실제 객체를 모르고 타입만을 이용해서 코드를 작성하는 일이 가능해집니다.
기존 SampleDAO를 interface로 바꾸고 SampleDAOImple클래스를 만들고 implements 시킵니다.
그리고 @Repository를 이용해서 해당 클래스의 객체를 스프링의 빈으로 처리되도록 구성합니다.
SampleService입장에서는 인터페이스만 바라보고 있기 때문에 실제 객체가 SampleDAOImpl의 인스턴스인지 알 수 없지만,
코드를 작성하는데 아무런 문제가 없습니다.
이처럼 객체와 의존관계의 실제 객체를 몰라도 가능하게 하는 방식을 '느슨한 결합(loose coupling)'이라고 합니다.
느슨한 결합을 이용하면 나중에 SampleDAO타입의 객체를 다른 객체로 변경하더라도 SampleService타입을 이용하는 코드를 수정할 일이 없기 때문에 유연한 구조라고 할 수 있습니다.
특정기간에만 SampleDAO를 다른 객체로 변경해야 하는 경우, EventSampleDAOImpl클래스를 만들고 implements 시킵니다.
이렇게되면 SampleService에 필요한 SampleDAO타입의 빈이 두개(SampleDAOImple, EventSampleDAOImpl)가 되기때문에,
스프링의 입장에서는 어떤것을 주입해야하는지 알수없게 됩니다. 또한 테스트코드를 실행하면 어떤 클래스의 객체를 사용해야 하는지 알수 없으므로 에러가 발생하게 됩니다.
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.zerock.springex.sample.SampleDAO' available: expected single matching bean but found 2: eventSampleDAOImple,sampleDAOImple : SampleDAO타입의 객체가 하나일길 기대했는데 2개가 발견되었다는 것입니다.
이를 해결하는 가장 간단한 방법은 두가지 있습니다.
1. @Primary : 두 클래스 중에 하나를 @Primary라는 어노테이션으로 지정해 주는 것입니다.
예를들어 EventSampleDAOImp을 지금 사용하고 싶다면 @Primary로 지정합니다.
@Repository
@Primary
public class EventSampleDAOImple implements SampleDAO{
}
2. @Qualifier : @Qualifier는 이름을 지정해서 특정한 이름의 객체를 주입받는 방식입니다.
Lombok과 @Qualifier를 같이 이용하기 위해서는 src/main/java폴더에 lombok.config파일을 생성합니다.
그리고 다음과 같이 작성합니다.
lombok.copyableannotations += org.springframework.beans.factory.annotation.Qualifier
동작을 확인하기 위해서 SampleDAOImpl과 EventSampleDAOImpl에 @Qualifier를 적용합니다.
@Repository
@Qualifier("normal")
public class SampleDAOImple implements SampleDAO {
}
@Repository
@Qualifier("event")
public class EventSampleDAOImple implements SampleDAO{
}
SampleService에는 특정한 이름의 객체를 사용하도록 수정합니다.
@Service
@ToString
@RequiredArgsConstructor
public class SampleService {
@Qualifier("normal")
private final SampleDAO sampleDAO;
}
8. 스프링의 빈으로 지정되는 객체들 :
스프링 프레임워크를 이용해서 객체를 생성하고 의존성 주입을 이용할 수 있다는 사실을 알았지만 작성되는 모든 클래스의 객체가 스프링의 빈으로 처리되는것은 아닙니다.
스프링의 빈으로 등록되는 객체들은 쉽게 말해서 '핵심배역'을 하는 객체들입니다.
스프링의 빈으로 등록되는 객체들은 주로 오랜시간 동안 프로그램 내에 상주하면서 중요한 역할을 하는 '역할'중심의 객체들입니다.
반대로말하면 DTO나 VO와 같이 '역할'보다는 '데이터'에 중점을 두고 설계된 객체들은 스프링의 빈으로 등록되지 않습니다.
특히 DTO의 경우 생명주기가 굉장히 짧고, 데이터 보관이 주된 역할이기 때문에 스프링의 빈으로 처리하지 않습니다.
XML이나 어노테이션으로 처리하는 객체
빈으로 처리할때 XML설정을 이용할 수도 있고, 어노테이션을 처리할 수도 있지만, 선택기준은 '코드를 수정할수 있는가'로 판단합니다.
예를들어 jar파일로 추가되는 클래스의 객체를 스프링의 빈으로 처리해야 한다면 해당 코드가 존재하지 않기 때문에 어노테이션을 추가할 수 없다는 문제가 생깁니다.이러한 객체들은 XML에서 <bean>을 이용해서 처리하고, 직접 작성되는 클래스는 어노테이션을 이용합니다.
9. 웹프로젝트를 위한 스프링 준비
스프링구조는 ApplicationContext라는 객체가 있고, 빈으로 등록된 객체들은 ApplicationContext내에 생성되서 관리되는 형태입니다.
이렇게 만들어진 ApplicationContext가 웹애플리케이션에서 동작하려면 웹애플리케이션이 실행될때 스프링을 로딩해서,
해당 웹 애플리케이션 내부에 ApplicationContext를 생성하는 작업이 필요하게 되는데,
이를 위해서는 web.xml을 이용해서 리스너를 설정합니다.
■ 프로젝트의 build.gradle의 파일에 spring-webmvc 라이브러리를 추가합니다.
dependencies {
... 생략 ...
implementation group: 'org.springframework', name: 'spring-core', version: '5.3.19'
implementation group: 'org.springframework', name: 'spring-context', version: '5.3.19'
testImplementation group: 'org.springframework', name: 'spring-test', version: '5.3.19'
implementation group: 'org.springframework', name: 'spring-webmvc', version: '5.3.19'
... 생략 ...
}
■ WEB-INF폴더 아래 web.xml에 <listener>설정과 <listener>에 필요한 <context-param>을 추가합니다.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
■ build.gradle에 MariaDB드라이버, HikariCP관련 라이브러리를 추가합니다.
dependencies {
... 생략 ...
// MariaDB, HikariCP
implementation 'org.mariadb.jdbc:mariadb-java-client:3.0.4'
implementation group: 'com.zaxxer', name: 'HikariCP', version: '5.0.1'
}
이전에는 HikariCP를 사용하기 위해서 HikariConfig객체와 HikariDataSource를 초기화 해야만 했습니다.
이를 위해서 ConnectionUtil이라는 클래스를 다음과 같이 작성했었습니다.
ConnectionUtil() {
HikariConfig config = new HikariConfig();
config.setDriverClassName("org.mariadb.jdbc.Driver");
config.setJdbcUrl("jdbc:mariadb://localhost:3306/webdb");
config.setUsername("webuser");
config.setPassword("webuser");
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
ds = new HikariDataSource(config);
}
스프링을 이용한다면 이설정은 스프링의 빈으로 처리되어야 합니다.
root-context.xml을 이용해서 HikariConfig와 HikariDataSource객체를 다음과 같이 설정합니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.zerock.springex.sample"/>
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="driverClassName" value="org.mariadb.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mariadb://localhost:3306/webdb"></property>
<property name="username" value="webuser"></property>
<property name="password" value="webuser"></property>
<property name="dataSourceProperties">
<props>
<prop key="cachePrepStmts">true</prop>
<prop key="prepStmrCacheSize">250</prop>
<prop key="prepStmrCacheSqlLimit">2048</prop>
</props>
</property>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<constructor-arg ref="hikariConfig" />
</bean>
</beans>
root-context.xml에 추가된 HikariConfig에는 id속성이 적용되어있고,
HikariDataSource에는 <constructor-arg ref="hikariConfig" />로 id값을 참조해서 사용하고 있습니다.
다음 테스트코드로 문제가 없는지 확인합니다.
@Autowired
private DataSource dataSource;
@Test
public void testConnection() throws Exception {
Connection connection = dataSource.getConnection();
log.info(connection);
Assertions.assertNotNull(connection);
connection.close();
}
root-context.xml에 선언된 HikariCP를 주입받기 위해 DataSource타입의 변수를 선언하고 @Autowired를 이용해서 주입받았습니다.
스프링은 필요한 객체를 스프링에서 주입해주기때문에 개별적으로 클래스를 작성해서 빈으로 등록해 두기만 하면 원하는 곳에서 쉽게 다른 객체를 사용할 수 있습니다.
이런 특징으로 인해 스프링 프레임워크는 웹이나 데이터베이스와 같이 특정한 영역이 아닌 전체 애플리케이션의 구조를 설계할 때 사용됩니다.
■ MyBatis연동하기
우선 MyBatis는 'Sql Mapping Framework'라고 표현됩니다. 이는 SQL의 실행결과를 객체지향으로 매핑해준다는 뜻입니다.
MyBatis를 이용하면 기존의 SQL을 그대로 사용할 수 있고 다음과 같은 편리한점이 있습니다.
▶ PreparedStatement/ResultSet의 처리 - 기존에 프로그램을 작성해서 하나씩 처리해야 하는 파라미터나 ResultSet의 GetXXX()를 MyBatis가 알아서 처리해 주기 때문에 많은 양의 코드를 줄일 수 있습니다.
▶ Connection/PreparedStatement/ResultSet의 close()처리 - MyBatis와 스프링을 연동해서 사용하는 방식을 이용하면 자동으로 close()처리할 수 있습니다.
▶ SQL의 분리 - MyBatis를 이용하면 별도의 파일이나 어노테이션 등을 이용해서 SQL을 선언합니다. 파일을 이용하는 경우에는 SQL을 별도의 파일로 분리해서 운영이 가능합니다.
MyBatis는 단독으로도 실행이 가능한 완전히 독립적인 프레임워크지만, 스프링프레임워크가 MyBatis와 연동을 쉽게 처리할 수 있는 라이브러리와 API들을 제공합니다. 스프링에서 제공하는 라이브러리를 이용하는지 여부에 따라서 다음 방식 중에 하나로 개발 가능합니다.
▶ MyBatis를 단독으로 개발하고 스프링에서 DAO를 작성해서 처리하는 방식 :
기존의 DAO에서 SQL의 처리를 MyBatis를 이용하는 구조로써 완전히 MyBatis와 스프링프레임워크를 독립적인 존재로 개발하는 방식
▶ MyBatis와 스프링을 연동하고 Mapper인터페이스만 이용하는 방식 :
스프링과 MyBatis사이에 'mybatis-spring' 라이브러리를 이용해서 스프링이 데이터베이스전체에 대한 처리를 하고 MyBatis는 일부기능개발에 활용하는 방식. 개발시에는 Mapper인터페이스라는 방식을 이용해서 인터페이스만으로 모든 개발이 가능한 방식
MyBatis를 이용하려면 다음과 같은 라이브러리들이 필요합니다.
스프링관련 : spring-jdbc, spring-tx
MyBatis관련 : mybatis, mybatis-spring
스프링관련 라이브러리들은 build.gradle에추가되어 있는 다른 라이브러리들과 버전을 같도록해서 추가합니다.
implementation group: 'org.springframework', name: 'spring-jdbc', version: '5.3.19'
implementation group: 'org.springframework', name: 'spring-tx', version: '5.3.19'
MyBatis관련 라이브러리는 검색을 이용해서 추가합니다. 버전은 일치하지 않으므로 주의해서 사용합니다.
//MyBatis
// https://mvnrepository.com/artifact/org.mybatis/mybatis
implementation group: 'org.mybatis', name: 'mybatis', version: '3.5.9'
// https://mvnrepository.com/artifact/org.mybatis/mybatis-spring
implementation group: 'org.mybatis', name: 'mybatis-spring', version: '2.0.7'
MyBatis를 이용하기 위해서는 스프링에 설정해둔 HikariDataSource를 이용해서 SqlSessionFactory라는 빈을 설정합니다.
root-context.xml에 'mybatis-spring'라이브러리에 있는 클래스를 이용해서 <bean>을 등록합니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.zerock.springex.sample"/>
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="driverClassName" value="org.mariadb.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mariadb://localhost:3306/webdb"></property>
<property name="username" value="webuser"></property>
<property name="password" value="webuser"></property>
<property name="dataSourceProperties">
<props>
<prop key="cachePrepStmts">true</prop>
<prop key="prepStmrCacheSize">250</prop>
<prop key="prepStmrCacheSqlLimit">2048</prop>
</props>
</property>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<constructor-arg ref="hikariConfig" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
앞의 설정이 완료된 상황에서 프로젝트가 정상적으로 실행되는지 확인하도록 합니다.
만일 'spring-jdbc', 'spring-tx'가 없다면 에러가 발생할 수 있으니 이단계에서 확인하도록 합니다.
■ Mapper 인터페이스 활용하기
MyBatis는 SQL파일을 별도로 처리할 수 있지만 인터페이스와 어노테이션만으로도 처리가 가능합니다.
프로젝트에 'mapper'패키지를 만들고 현재시간을 처리하는 TimeMapper 인터페이스를 선언합니다.
package org.zerock.springex.mapper;
import org.apache.ibatis.annotations.Select;
public interface TimeMapper {
@Select("select now()")
String getTime();
}
TimeMapper는 데이터베이스의 현재시각을 문자열로 처리하도록 구성합니다.
MyBatis에는 @Select 어노테이션을 이용해서 쿼리를 작성하는데 JDBC와 마찬가지로 ';'를 이용하지 않음에 주의합니다.
작성된 인터페이스를 매퍼인터페이스라고 하는데, 마지막으로 어떠한 매퍼인터페이스를 설정했는지 root-context.xml에 등록합니다.
root-context.xml에는 <mybatis:scan>태그를 이용해서 매퍼인터페이스의 설정을 추가합니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">
<context:component-scan base-package="org.zerock.springex.sample"/>
... 생략 ...
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
<mybatis:scan base-package="org.zerock.springex.mapper"></mybatis:scan>
</beans>
상단의 xmlns, xsi설정에 mybatis-spring관련 설정이 추가되는데 인텔리제이에서 자동으로 추가시켜줍니다.
만약 되지않는다면 위와같이 추가하도록 합니다.
다음 테스트코드를 통해 확인합니다.
package org.zerock.springex.mapper;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TimeMapperTests {
@Autowired(required = false)
private TimeMapper timeMapper;
@Test
public void testGetTime() {
log.info(timeMapper.getTime());
}
}
테스트코드에서는 @Autowired내에 required속성을 지정했습니다. @Autowired(required=false)로 지정하면,
해당 객체를 주입 받지 못하더라도 예외가 발생하지 않는데 인텔리제이의 경우 @Service, @Repository... 와 같이 직접 스프링의 빈으로 등록된 경우가 아니면 그림과 같은 경고가 발생하므로 이를 방지하기 위해서 사용합니다.
MyBatis와 스프링을 연동하고 매퍼인터페이스를 활용하는 방식은 개발자가 실제 동작하는 클래스와 객체를 생성하지 않고,
스프링에서 자동으로 생성되는 방식을 이용하게 됩니다. 스프링에서 자동으로 생성된 객체를 이용하기 때문에 개발자가 직접 코드를 수정할 수는 없지만 인터페이스만으로도 개발을 완료할수 있다는 장점이 있습니다.
■ XML로 SQL분리하기
MyBatis를 이용할 때 SQL은 @Select와 같은 어노테이션을 이용해서 사용하기도 합니다만 대부분은 SQL을 별도의 파일로 분리하는것을 권장합니다. XML을 이용하는 이유는 SQL이 길어지면 이를 어노테이션으로 처리하기가 복잡해지기 때문이기도하고 어노테이션이 나중에 변경되면 프로젝트 전체를 다시 빌드하는 작업이 필요하기 때문에 단순 파일로 사용하는것이 편리합니다.
XML과 매처 인터페이스를 같이 결합할 때는 다음과 같은 과정으로 작성합니다.
▶ 매퍼인터페이스를 정의하고 메소드를 선언
▶ 해당 XML파일을 작성(파일이름과 매퍼인터페이스 이름을 같게)하고 <select>같은 태그를 이용해서 SQL을 작성
▶ <select>, <insert>등의 태그에 id속성값을 매퍼 인터페이스의 메소드이름과 같게 작성
public interface TimeMapper2 {
String getNow();
}
<?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="org.zerock.springex.mapper.TimeMapper2">
<select id="getNow" resultType="String">
select now()
</select>
</mapper>
TimeMapper2.xml을 작성할때는 <mapper>태그의 namespace 속성을 반드시 매퍼인터페이스의 이름과 동일하게 지정합니다.
<select>태그는 반드시 resultType이나 resultMap이라는 속성을 지정해야합니다.
resultType은 말그대로 select문의 결과를 어떤 타입으로 처리할지에 대한 설정으로 java.lang.String과 같이 전체 이름을 써야 하지만
자주 사용하는 타입은 string과 같이 사용할수 있습니다.
마지막으로 root-context.xml에 있는 Mybatis설정에 XML파일들을 인식하도록 설정을 추가합니다.
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:/mappers/**/*.xml"></property>
</bean>
추가된 mapperLocations는 말그대로 XML매퍼 파일들의 위치를 의미합니다. resources의 경우 'classpath:'접두어를 이용해서 인식되는 경로이고 mappers폴더밑에 폴더가 있어도 관계없도록 '**'와 모든 '.xml'을 의미하는 '*.xml'을 지정합니다.
'개발 > JAVA' 카테고리의 다른 글
[자바웹개발워크북] 스프링 MVC 구현 - 환경설정, CRUD (0) | 2023.01.17 |
---|---|
[자바웹개발워크북] 4-2. 스프링 Web MVC (0) | 2023.01.14 |
[자바웹개발워크북] 3. 세션/쿠키/필터/리스너 (0) | 2022.12.28 |
[자바웹개발워크북] 2. 웹과 데이터베이스 (0) | 2022.12.24 |
[자바웹개발워크북] 1. 웹프로그래밍의 시작 (0) | 2022.12.16 |