1. 스프링
1) 의존성 주입
- 어떻게 하면 객체와 객체 간의 관계를 더 유연하게 유지할 것인가(객체의 생성과 관계를 효과적으로 분리할 수 있는가)
- 예를 들어, Todo 웹 애플리케이션을 만들 때, 모든 Controller는 TodoService 혹은 MemberService같은 서비스 객체 이용 → Controller는 서비스 객체에 의존적이다
- 즉, 의존성이란 하나의 객체가 자신이 해야하는 일을 하기 위해 다른 개체의 도움이 필수적인 관계
- 의존성을 해결하기 위해 스프링 프레임워크 사용
2) 스프링 라이브러리 추가
- maven spring 검색하여 Spring Core 라이브러리를 찾아, Gradle 메뉴의 코드 복사
- 프로젝트의 build gradle 파일에 해당 코드 복사, spring-core에 더해서 spring-context, spring-test도 추가
- lombok 라이브러리, Log4j2 라이브러리, JSTL 라이브러리 추가
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}")
// spring 관련 라이브러리
implementation group: 'org.springframework', name: 'spring-core', version: '5.3.20'
implementation group: 'org.springframework', name: 'spring-context', version: '5.3.20'
implementation group: 'org.springframework', name: 'spring-test', version: '5.3.20'
// lombok 라이브러리
compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
testCompileOnly 'org.projectlombok:lombok:1.18.24'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.24'
// Log4j2 라이브러리
implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.17.2'
implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.17.2'
implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.17.2'
// JSTL 라이브러리
implementation group: 'jstl', name: 'jstl', version: '1.2'
}
- main > 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.SSS} %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>
실습
1) 의존성 주입하기
- 프로젝트에 sample 패키지 > SampleService와 SampleDAO 클래스 생성
- 스프링 프레임워크는 자체적으로 객체를 생성하고 관리하면서 필요한 곳에 객체를 주입시키는 역할(설정파일이나 어노테이션 이용)
- 스프링이 관리하는 객체들은 빈(Bean)이라는 이름으로 불림, 프로젝트 내에 어떤 빈들을 어떻게 관리할 것인지 설정하는 설정 파일 작성
- XML 설정이나 별도의 클래스를 이용한 자바 설정이 가능
- XML 설정을 위해 WEB-INF 폴더 > New > XML Configuration File > Spring config > root-context.xml 파일 생성
- Configuration application context 설정에서 인텔리제이가 현재 프로젝트를 스프링 프레임워크로 인식하고 필요한 기능을 지원하도록 설정
- root-context.xml 내부에 <bean>이라는 태그를 이용해서 SampleService와 SampleDAO를 다음과 같이 설정
- 스프링의 빈 설정 테스트
- test 폴더 > org.zerock.springex.sample 패키지 생성 > SampleTest 클래스 추가
package org.zerock.springex.sample;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Assertions;
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은 JUnit5 버전에서 spring-test를 이용하기 위한 설정
// ContextConfiguration은 스프링의 설정 정보를 로딩하기 위해 사용
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class SampleTest {
// SampleService를 멤버 변수로 선언
// Autowired는 스프링에서 의존성 주입 관련 어노테이션(만약 해당 타입의 빈이 존재하면 여기에 주입하기를 원한다)
@Autowired
private SampleService sampleService;
@Test
public void testService1() {
log.info(sampleService);
Assertions.assertNotNull(sampleService);
}
}
2. ApplicationContext와 빈(Bean)
- Servlet이 Servlet Context 안에 존재한 것처럼, 스프링은 Bean을 관리하기 위해 ApplicationContext라는 존재 활용
- root-context.xml에서 SampleService와 SampleDAO를 <bean>으로 설정하여 다음과 같이 저장됨
1) @Autowired의 의미와 필드 주입
- Test 코드에서 SampleService 타비의 변수가 선언될 때, @Autowired로 처리됨
- Test 실행 시 @Autowired가 처리된 부분에 맞는 타입의 Bean이 존재하는지 확인하고 Test 실행시 주입
- 멤버 변수에 직접 @Autowired를 선언하는 방식을 '필드 주입'이라고 함
실습
2) SampleDAO 주입하기
- @Autowired를 이용하면 필요한 타입을 주입받을 수 있다는 사실을 이용, SampleService에 SampleDAO를 주입
// SampleService
package org.zerock.springex.sample;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
@ToString
public class SampleService {
@Autowired
private SampleDAO sampleDAO;
}
- Test 실행 시 SampleService안에 sampleDAO 객체가 주입된 것을 확인
2) <context:component-scan>
- 스프링 이용시, 클래스를 작성하거나 객체를 직접 생성하는 역할은 스프링 내부에서 이루어지며 applicationContext가 생성된 객체들을 관리하게 됨
실습
2) @Service, @Repository
- 스프링 프레임워크 사용을 위한 어노테이션
- @Controller: MVC의 Controller를 위한 어노테이션
- @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">
<!-- component-scan의 속성값으로 패키지를 지정 -->
<!-- 해당 패키지를 스캔해서 스프링의 어노테이션들을 인식함 -->
<context:component-scan base-package = "org.zerock.springex.sample"/>
</beans>
- SampleDAO는 해당 클래스의 객체가 스프링의 Bean으로 관리될 수 있도록 @Repository 어노테이션 추가
// SampleDAO
package org.zerock.springex.sample;
import org.springframework.stereotype.Repository;
@Repository
public class SampleDAO {
}
- SampleService는 @Service 어노테이션 추가
// SampleService
package org.zerock.springex.sample;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@ToString
public class SampleService {
@Autowired
private SampleDAO sampleDAO;
}
3) 생성자 주입 방식
- 초기 스프링에서는 @Autowired를 멤버 변수에 할당하거나 Setter를 작성하는 방식을 많이 이용
- 스프링3 이후, 생성자 주입 방식 이용
- 생성자 주입 방식 규칙
- 주입 받아야 하는 객체의 변수는 final로 작성
- 생성자를 이용해서 해당 변수를 생성자의 파라미터로 지정
- 생성자 주입 방식은 객체를 생성 시 문제가 발생하는지 미리 확인할 수 있어, 필드 주입이나 Setter 주입 방식보다 선호
- Lombok에서 @RequiredArgsConstructor를 이용해 필요한 생성자 함수를 자동으로 작성 가능
- SampleService를 다음과 같이 변경하면 @Autowired로 필드 주입한 것과 같이 SampleDAO를 주입 가능
// SampleService
package org.zerock.springex.sample;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@ToString
@RequiredArgsConstructor
public class SampleService {
private final SampleDAO sampleDAO;
}
3. 인터페이스를 이용한 느슨한 결합
- 스프링이 의존성 주입을 가능하게 하지만 더 근본적으로 유연한 프로그램 설계를 위해 인터페이스를 이용
- 인터페이스 이용 시, 나중에 다른 클래스의 객체로 쉽게 변경할 수 있도록 함
- 앞에서 SampleDAO를 다른 객체로 변경하면 SampleService의 코드도 수정해야했지만, 인터페이스를 이용하면 실제 객체를 모르고 타입만을 이용해서 코드를 작성하는 일이 가능
실습
4) SampleDAO를 인터페이스로 변경하기
- 클래스로 작성된 SampleDAO를 인터페이스 타입으로 수정
// SampleDAO
package org.zerock.springex.sample;
import org.springframework.stereotype.Repository;
@Repository
public interface SampleDAO {
}
- SampleDAO 인터페이스는 실체가 없기 때문에 SampleDAO 인터페이스를 구현한 클래스를 SampleDAOImpl이라는 이름으로 선언
// SampleDAOImpl
package org.zerock.springex.sample;
import org.springframework.stereotype.Repository;
// @Repository를 이용해서 해당 클래스의 객체를 스프링의 Bean으로 처리하도록 구성
@Repository
public class SampleDAOImpl implements SampleDAO{
}
- SampleService 입장에서는 인터페이스만 바라보고 있기 때문에 실제 객체가 SampleDAOImpl의 인스턴스인지 알 수 없지만, 코드 작성에 문제 x
- 느슨한 결합: 객체와 객체의 의존 관계의 실제 객체를 몰라도 가능하게 하는 방식
- 느슨한 결합을 이용하면 나중에 SampleDAO 타입의 객체를 다른 객체로 변경해도 SampleService 타입을 이용하는 코드를 수정할 일이 없어 더 유연한 구조임
- 다른 SampleDAO 객체로 변경해보기(특정 기간에만 SampleDAO를 다른 객체로 변경해야 하는 경우 생각)
- EventSampleDAOImpl 클래스 작성
// EventSampleDAOImpl
package org.zerock.springex.sample;
import org.springframework.stereotype.Repository;
@Repository
public class EventSampleDAOImpl implements SampleDAO{
}
- SampleService에 필요한 SampleDAO 타입의 Bean이 두 개(SampleDAOImpl, EventSampleDAOImpl)이므로, 어떤걸 주입해야 할 지 모르게 됨
- Test 실행 시, 스프링이 SampleDAO 타입의 객체가 하나이길 기대했지만 2개가 발견됐다는 오류 출력
- 해결 방법으로 두 클래스 중 하나를 @Primary라는 어노테이션으로 지정(지금 사용하고 싶은 것에 지정)
// EventSampleDAO
package org.zerock.springex.sample;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;
@Repository
@Primary
public class EventSampleDAOImpl implements SampleDAO{
}
- Test 실행 시, 정상적으로 EventSampleDAOImpl이 주입된 것을 확인
- Qualifier 이용하기
- @Primary 이용하는 방법 이외에 @Qualifier를 이용하여 이름을 지정해서 특정한 이름의 객체 주입
- Lombok과 @Qualifier를 같이 이용하기 위해 src/main/java 폴더에 lombok.config 파일 생성
// lombok.config
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
// SampleDAOImpl
@Repository
// @Qualifier를 이용해서 SampleDAOImpl에 'normal'이라는 이름 지정
@Qualifier("normal")
public class SampleDAOImpl implements SampleDAO{
}
// EventSampleDAOImpl
@Repository
// @Qualifier를 이용해서 EventSampleDAOImpl에 'event'라는 이름 지정
@Qualifier("event")
public class EventSampleDAOImpl implements SampleDAO{
}
- SampleService에서 @Qualifier를 이용해 이름을 지정하면 해당 이름의 SampleDAO 객체를 사용
// SampleService
package org.zerock.springex.sample;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
@ToString
@RequiredArgsConstructor
public class SampleService {
@Qualifier("normal")
private final SampleDAO sampleDAO;
}
// SampleService
package org.zerock.springex.sample;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
@ToString
@RequiredArgsConstructor
public class SampleService {
@Qualifier("event")
private final SampleDAO sampleDAO;
}
- 스프링의 Bean으로 지정되는 객체들
- 스프링의 모든 클래스의 객체가 Bean으로 처리되는 것은 x
- 스프링의 Bean으로 등록되는 객체들은 '핵심 배역'을 하는 객체(오랜 시간 프로그램에 상주하며 중요한 역할을 하는 '역할 중심' 객체
- DTO나 VO 등 '역할'보다 '데이터' 중심으로 설계된 객체들은 스프링의 Bean으로 등록되지 않음(특히 DTO는 생명주기가 짧고 데이터 보관이 주된 역할이어서 Bean으로 처리 x)
- XML이나 어노테이션으로 처리하는 객체
- Bean으로 처리할 때 XML 설정을 이용할 수도 있고 어노테이션을 처리할 수도 있음
- 판단 기준은 '코드를 수정할 수 있는가'
- jar 파일로 추가되는 클래스의 객체를 Bean으로 처리해야 하면, 해당 코드가 존재하지 ㅇ낳아 어노테이션을 추가할 수 없어, XML에서 <bean>을 사용해 처리
- 직접 작성되는 클래스는 어노테이션을 이용
4. 웹 프로젝트를 위한 스프링 준비
- Bean을 담은 ApplicationContext가 웹 애플리케이션에서 동작하려면, 웹 애플리케이션이 실행될 때 스프링을 로딩해서 해당 웹 애플리케이션 내부에 스프링의 ApplicationContext를 생성하는 작업 필요
- web.xml을 이용해서 리스너를 설정
- web.xml 설정 이전에, 스프링 프레임워크의 웹 관련 작업은 spring-webmvc 라이브러리를 추가해야 설정 가능
// build.gradle 파일에 spring-webmvc 라이브러리 추가
dependencies {
...
implementation group: 'org.springframework', name: 'spring-core', version: '5.3.20'
implementation group: 'org.springframework', name: 'spring-context', version: '5.3.20'
implementation group: 'org.springframework', name: 'spring-test', version: '5.3.20'
implementation group: 'org.springframework', name: 'spring-webmvc', version: '5.3.20'
...
}
- web.xml에 <listener> 설정과 이에 필요한 <context-param> 추가
<!-- web.xml -->
<?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>
- 위의 설정을 한 뒤에 톰캣 실행 시, 스프링 관련 로그가 기록되며 실행
실습
5) DataSource 구성
- 톰캣과 스프링이 연동되는 구조를 완성하면, 웹 애플리케이션에서 필수인 데이터베이스 관련 설정 추가 필요
- build.gradle에 MariaDB 드라이버와 HikariCP 관련 라이브러리 추가
// build.gradle
dependencies {
...
implementation 'org.mariadb.jdbc:mariadb-java-client:3.0.4'
implementation group: 'com.zaxxer', name: 'HikariCP', version: '5.0.1'
}
- root-context.xml에 HikariCP 설정하기
- 스프링에서는 HikariCP 사용을 위해 HikariConfig 객체와 HikariDataSource 초기화 설정을 Bean으로 처리(이전에는 ConnectUtil 클래스 사용)
<!-- 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">
<!-- component-scan의 속성값으로 패키지를 지정 -->
<!-- 해당 패키지를 스캔해서 스프링의 어노테이션들을 인식함 -->
<context:component-scan base-package = "org.zerock.springex.sample"></context:component-scan>
<!-- db와 connection pool 형성을 위한 hikariCP를 구성 -->
<!-- hikariConfig를 이용해서 HikariDataSource를 구성 -->
<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 = "비밀번호"></property>
<property name = "dataSourceProperties">
<props>
<!-- cache 사용 여부에 대한 설정 -->
<prop key = "cachePrepStmts">true</prop>
<!-- 서버 연결 당 cache할 statement의 수에 관한 설정(기본값 25, 권장 250~500 -->
<prop key = "prepStmtCacheSize">250</prop>
<!-- 드라이버가 cache할 SQL문의 최대 길이(기본값 256, 권장 2048 -->
<prop key = "prepStmtCacheSqlLimit">2048</prop>
</props>
</property>
</bean>
<!-- HikariDataSource는 <constructor-arg ref='hikariConfig" />로 id값을 참조해서 사용 -->
<bean id = "dataSource" class = "com.zaxxer.hikari.HikariDataSource" destroy-method = "close">
<constructor-arg ref = "hikariConfig" />
</bean>
</beans>
- SampleTest에는 root-context.xml에 선언된 HikariCPp를 주입받기 위해 DataSource 타입의 변수를 선언, @Autowired를 통해 주입 받음
- testConnection()을 작성하여 데이터베이스와의 연결을 test
// SampleTest
package org.zerock.springex.sample;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Assertions;
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;
import javax.sql.DataSource;
import java.sql.Connection;
@Log4j2
// ExtendWith은 JUnit5 버전에서 spring-test를 이용하기 위한 설정
// ContextConfiguration은 스프링의 설정 정보를 로딩하기 위해 사용
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class SampleTest {
...
@Autowired
private DataSource dataSource;
...
@Test
public void testConnection() throws Exception {
Connection connection = dataSource.getConnection();
log.info(connection);
Assertions.assertNotNull(connection);
connection.close();
}
}
- 테스트에서 보이듯, 스프링은 필요한 객체를 스프링에 주입해 주기 때문에 개별적으로 클래스를 작성하여 Bean으로 등록해두면 원하는 곳에서 쉽게 다른 객체 사용 가능
'back-end > Java' 카테고리의 다른 글
[자바 웹 개발 워크북] 4.3 - 스프링 Web MVC 기초 (0) | 2023.01.17 |
---|---|
[자바 웹 개발 워크북] 4.2 - MyBatis와 스프링 연동 (0) | 2023.01.13 |
[자바 웹 개발 워크북] 3.3 - 리스너 (0) | 2023.01.11 |
[자바 웹 개발 워크북] 3.2 - 사용자 정의 쿠키 (1) | 2023.01.11 |
[자바 웹 개발 워크북] 3.1 - 세션과 필터 (0) | 2023.01.09 |