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 객체가 주입된 것을 확인

SampleDAO의 의존성이 주입된 SampleService


 

 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에서 @Qualifier의 이름을 normal로 지정했을 때 SampleDAOImpl을 사용

// 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;
}

 

SampleService에서 @Qualifier의 이름을 event로 지정했을 때 EventSampleDAOImpl을 사용

 

  - 스프링의 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();
  }
}

testConnection의 결과, 데이터베이스와 정상적으로 연결되었다고 출력된 로그

  - 테스트에서 보이듯, 스프링은 필요한 객체를 스프링에 주입해 주기 때문에 개별적으로 클래스를 작성하여 Bean으로 등록해두면 원하는 곳에서 쉽게 다른 객체 사용 가능

+ Recent posts