본문 바로가기
웹개발/스프링

스프링 4 비즈니스 로직 설계와 트랜잭션

by 인생여희 2020. 11. 9.

트랜잭션이란?

트랜잭션은 관련된 여러 처리를 하나의 큰 처리로 취급할 경우의 단위이다.

 

트랜잭션의 경계

트랜잭션의 경계는 프레젠테이션 층과 비즈니스 로직 층 사이에 그어지는것이 일반적이다. 프레젠테이션 층에 공개된 서비스 클래스의 메서드가 트랜잭션의 시작과 종료라고 할 수 있다. 다시 말하면, 컨트롤러에서 서비스 클래스의 메서드가 호출되면 트랜잭션 시작, 서비스 클래스의 메서드를 마치고 컨트롤러로 되돌아 갈때가 트랜잭션의 종료이다.

 

트랜잭션 처리를 구현하는 장소 문제

트랜잭션 처리의 API(커밋, 롤백)는 데이터 액세스 기술(JDBC, 하이버네이트)에 따라 달라진다. 예를들어 JDBC를 이용했을 때, 트랜잭션의 커밋이나 롤백 같은 메서드는 java.sql.Connection에 있다. 따라서 비즈니스 로직에서 트랜잭션을 처리할려고 하면, 비즈니스 로직 안에서 커넥션을 취득하거나 커밋/롤백을 호출해야 되므로, 원래는 은닉되어야하는 JDBC의 API에 비즈니스 로직층이 의존을 하게 된다.  또한 SQL을 발행하는 계좌 DAO가 사용하는 커넥션은 비즈니스 로직에서 가져온 커넥션을 공유해야 하므로 update 메서드의 인수로 커넥션을 건네주는 등의 대응이 필요하다.

 

AOP를 이용한 트랜잭션 처리

AOP를 이용해서 서비스에 어드바이스를 적용함으로써 서비스 내부를 수정하지 않고 트랜잭션 처리를 구현할 수 있다. 트랜잭션 처리를 구현한 어드바이스를 직접 만들필요는 없다. 스프링이 제공하는 트랜잭션 매니저와 트랜잭션 어드바이스를 이용하면 된다.

 

트랜잭션 매니저

트랜잭션 매니저는 스프링이 제공하는 트랜잭션 처리를 위한 부품이다. 트랜잭션의 시작과 종료, 롤백 처리를 비롯해 트랜잭션의 정의 정보(롤백조건, 독립성 레벨등)를 세밀하게 설정할 수 있다. 또한 데이터 액세스 기술(JDBC, 하이버네이트 , Mybatis)를 은닉해 주므로 데이터 액세스 기술이 바뀌어도 같은 방법으로 트랜잭션 매니저를 이용할 수 있다.

 

트랜잭션 정의 정보는 아래와 같다.

 

1.전파속성

2.독립성 수준

3.타임아웃

4.읽기전용

5.롤백 대상 예외

6.커밋 대상 예외

 

전파속성

전파속성은 트랜잭션의 전파 방법을 설정하는 속성이다. 

컨트롤러 1에서 서비스 1의 메소드를 호출할경우, 트랜잭션은 서비스 1의 메소드가 호출됐을때 시작된다. 만약 컨트롤러 2에서 서비스 2를 호출하고 서비스 2에서 서비스1을 호출할 때는 바로 서비스 2의 메서드가 호출됐을 때 트랜잭션이 시작되고, 그 트랜잭션 안에서 서비스 1이 호출된다. 

 

이때 서비스 1의 메서드가 호출되면 동시에 트랜잭션을 새로 시작할지 아니면 원래 트랜잭션을 그대로 이어갈지를 선택해야 하는데 이때 트랜잭션의 전파를 설정하는 것이 전파 속성이다. 참고

 

전파 속성의 종류

전파속성의 종류

- REQUIRED : 기본적 속성. 이미 트랜잭션이 있으면 참여하고 없으면 새로 시작한다.

- REQUIRED_NEW : 무조건 새로운 트랜잭션을 시작한다. 이미 진행 중인 트랜잭션이 있으면 보류한다.

- MANDATORY : 트랜잭션이 있으면 참여한다. 하지만 트랜잭션이 없으면 예외를 발생시킨다.

- SUPPORTS : 이미 시작한 트랜잭션이 있으면 참여한다. 만약 그렇지 않다면 트랜잭션 없이 진행한다.

- NOT_SUPPORTED : 트랜잭션을 사용하지 않게 한다. 이미 진행 중인 트랜잭션이 있다면 보류한다.

- NEVER : 트랜잭션을 사용하지 않게 하고 이미 진행 중인 트랜잭션이 있다면 예외를 발생시킨다. 

- NESTED : 이미 진행 중인 트랜잭션이 있다면 중첩 트랜잭션을 시작한다.

 

예시상황

- 항상 최초로 호출되는 발주 서비스 오브젝트는 모두 REQUIRED로 설정.

 

- 단독으로 트랜잭션이 완결되고 다른 많은 트랜잭션이 병행해서 이용하는 번호 할당 서비스 오브젝트의 발주 번호 부여 메서드는 REQUIRED_NEW로 설정.

 

- 트랜잭션에 포함되지 않고 이용되기도 하는 고객 서비스 오브젝트의 검색 메소드는 SUPPORTS로 설정.

 

- 다른 서비스에서 이용되는 것이 전제고 트랜잭션이 시작되지 않으면 이용할 수 없는 재고 서비스 오브젝트의 재고 수량 감소 매서드는 MANDATORY로 설정.

 

 

독립성 수준

독립성 수준은 트랜잭션 처리가 병행해서 실행될 때 각 트랜잭션의 독립성을 결정하는 것이다.

트랜잭션 1과 트랜잭션 2가 나란히 실행됐다는 전제로, 이때 트랜잭션 1이 무엇인가 데이터 베이스의 레코드를 갱신했다고 하자. 단, 트랜잭션 도중이므로 아직 커밋은 하지 않았다. 오류가 발생하면 롤백해서 원래대로 돌아가는 불확실한 상태의 데이터이다. 이어서 트랜잭션 2가 트랜잭션 1에서 갱신한 데이터를 읽어오려고 한다. 읽어오려는 데이터는 커밋되지 않은 불확정한 상태의 데이터다. 이때 트랜잭션 2는 갱신된 데이터를 읽어와도 되는가 하는 문제가 발생한다. 모순 되지 않게 처리하려면 트랜잭션 1이 커밋해서 확정된 후 데이터를 읽어와야 한다. 이처럼 트랜잭션 1과 트랜잭션 2가 나란히 실행됐을 때 모순되지 않게 처리하는 속성이 독립성이다.

 

트랜잭션의 독립성 종류 - (출처)

독립성 수준과 데이터가 모순된 상태

1.  DEFAULT

 데이터 베이스에서 설정된 기본 격리 수준을 따릅니다.

 

2. READ_UNCOMMITED

트랜잭션이 아직 커밋되지 않은 데이터를 읽을 수 있습니다. 예를 들어 A라는 트랜잭션에서 데이터를 수정하고, 커밋이 아직 완료되지 않은 상황에서 다른 트랜잭션이 수정중인 데이터를 읽을 수 있습니다. 이러한 경우를 Dirty Read 라고 합니다. Drity Read 는 READ_UNCOMMITED 격리 수준에서 발생합니다.

 

3. READ_COMMITED

Dirty Read 를 방지하기 위해 Commit 된 데이터만 읽을 수 있습니다. 하지만 NON-REPEATABLE READ 가 발생할 수 있습니다. NON-REPEATABLE READ 는 A 트랜잭션이 a 데이터를 조회 중인데, 중간에 B 트랜잭션이 a 데이터를 수정하고 커밋하게되면 A 트랜잭션이 다시 a 데이터를 조회했을 때, 수정된 데이터가 조회되는 상태입니다. 이름 그대로 반복해서 같은 데이터를 읽을 수 없는 상태를 말합니다.

 

4. REPEATABLE READ

트랜잭션이 완료될 때까지 조회한 모든 데이터에 shared lock이 걸리므로 트랜잭션이 종료될 때까지 다른 트랜잭션은 그 영역에 해당하는 데이터를 수정할 수 없습니다. 먼저 수행된 트랜잭션은 처음 읽은 데이터가 종료될 때까지 같은 데이터를 조회할 수 있으므로 일관성 있는 결과를 리턴할 수 있습니다. 즉, REPEATABLE READ 격리 수준은 NON-REPEATABLE READ 를 허용하지 않습니다. 하지만 PHANTOM READ 상태가 발생할 수 있습니다. PHANTOM READ 는 A 트랜잭션이 10살 이하의 회원을 조회했는데 다른 트랜잭션이 5살 회원을 추가하고 커밋하면 A 트랜잭션이 다시 조회했을 때 회원 하나가 추가된 상태로 조회됩니다. 이와 같이 반복해서 조회했을 때, 결과 집합이 달라지는 상태를 PHANTOM READ 라고 합니다.

 

5. SERIALIZABLE

가장 엄격한 트랜잭션 격리수준으로, 완벽한 읽기 일관성 모드를 제공합니다. 이 격리 수준에서는 PHANTOM READ 상태가 발생하지 않지만 동시성 처리 성능이 급격히 떨어질 수 있습니다.

 

그 밖의 트랜잭션의 정의 정보

 

- 만료시간: 트랜잭션이 취소되는 만료 시간을 초 단위로 설정

 

- 읽기 전용 상태 : 트랜잭션 내의 처리가 읽기 전용으로 설정 된다. 이 설정 때문에 DB나 ORM프레임워크 쪽에서 최적화가 이루어진다.

 

- 롤백대상예외 : 어느 예외가 던져졌을 때 롤백할지 설정할 수 있다. 기본적으로 런타임 예외가 던져지면 롤백이 이루어지고, 검사 예외는 던져저도 롤백되지 않는다.

 

- 커밋대상예외 : 어느 예외가 던져졌을 때 커밋할지 설정 할 수 있다. 기본적으로 검사 예외가 던져졌을 때 커밋이 이루어진다.

 

트랜잭션 매니저의 구현 클래스

트랜잭션 매니저의 이용 방법은 테이터 액세스 기술에 의존하지 않는다. 이는 데이터 액세스 기술별로 제공되는 트랜잭션 매니저의 구현 클래스가 공통 인터페이스를 구현함으로써 실현되다.

공통 인터페이스는 PlatformTrasactionManager 라는 인터페이스다.

 

애플리케이션 개발자는 사용할 데이터 액세스 기술에 맞게 적절한 트랜잭션 매니저의 구현 클래스를 선택하고 Bean 파일에 등록한다. 아래는 DataSourceTransactionManager를 사용할때의 Bean 정의 파일이다.

    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

위 처럼 정의했으면 트랜잭션 기능을 사용할 준비가 끝났다.

 

Bean 정의 파일에 의한 선언적 트랜잭션

먼저 tx 스키마를 사용해서 트랜잭션의 어드바이스를 설정한다. 그리고 트랜잭션 처리를 하는 클래스와 인터페이스를 지정한다.

<?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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx" 
    xmlns:jdbc="http://www.springframework.org/schema/jdbc" 
    xmlns:jee="http://www.springframework.org/schema/jee"
    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
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">


    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
    
    
    <!-- 트랜잭션 처리를 하는 클래스와 인터페이스를 지정 -->
<!--     특별한 이유가 없다면 execution을 이용한다.
         임의의 패키지, 클래스 및 인터페이스 끝에 Service가 있는 모든 것을 지정한다.
         메서드 단위의 설정도 가능하지만, 트랜잭션 정의 정보의 설정에서 메서드 단위의 설정을 하므로 
         여기서는 클래스 및 인터페이스 단위로 해둔다. -->
    <aop:config>
        <aop:advisor advice-ref="transactionAdvice"
            pointcut="execution(* *..*Service.*(..))"
            />
    </aop:config>
    
    
    <!-- 트랜잭션 정의 정보 설정 
    get, update로시작하는 메서드에 대한 기본 트랜잭션 정의 정보로 트랜잭션이 이루어진다. -->
    
    <!-- 전파속성, 독립성수준, 만료시간, 읽기전용상태, 롤백대상예외에 대한 설정이 있다. -->
    <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="get*" read-only="true" />
            <tx:method name="update*" 
                propagation="REQUIRED"        
                isolation="READ_COMMITTED"
                timeout="10" 
                read-only="false"
                rollback-for="sample.biz.exception.BusinessException" />
        </tx:attributes>
    </tx:advice>
    
    

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg ref="dataSource" />
    </bean>

	<!-- 데이터 소스 -->
	<jdbc:embedded-database id="dataSource" type="HSQL">
	  <jdbc:script location="script/table.sql"/>
	  <jdbc:script location="script/data.sql"/>
	</jdbc:embedded-database>

    <context:component-scan base-package="sample"/>

</beans>

TranByFileMain - (호출 부분) 

package sample;
import java.util.Date;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import sample.biz.domain.Pet;
import sample.biz.service.PetService;

public class TranByFileMain {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("sample/config/spring-tranByFile.xml");
        PetService petService = ctx.getBean(PetService.class);
        Pet pet = new Pet();
        pet.setPetId(1);
        pet.setPetName("나비");
        pet.setOwnerName("홍길동");
        pet.setPrice(10000);
        pet.setBirthDate(new Date());
        
        petService.updatePet(pet);

    }
}

  


 

어노테이션에 의한 선언적 트랜잭션 예제

PetServiceImpl.java

package sample.biz.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import sample.biz.dao.PetDao;
import sample.biz.domain.Pet;
import sample.biz.exception.BussinessException;
import sample.biz.service.PetService;

@Service
public class PetServiceImpl implements PetService {

    @Autowired
    private PetDao petDao;

    
    
    //명시적 트랜잭션
    //호출할 메서드는 트랜잭션 매니저 인터페이스의 PlatformTransactionManage에 있다.
    //Bean정의 파일에 트랜잭션이 등록됐을 것이므로 트랜잭션 매니저의 Bean을처리하는 Bean에 인잭션 하면 된다.
    @Autowired
    private PlatformTransactionManager txManager;

    //어노테이션을 이용한 트랜잭션 처리
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, timeout = 10, readOnly = false, rollbackFor = BussinessException.class)
    public void updatePet(Pet pet) throws BussinessException {
        petDao.updatePet(pet);
    }


}

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:aop="http://www.springframework.org/schema/aop"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc" 
    xmlns:tx="http://www.springframework.org/schema/tx" 
    xmlns:jee="http://www.springframework.org/schema/jee"
    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
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">


    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
    
	<!-- @Transactional 사용을 유효하게 만드는 설정 -->
	<tx:annotation-driven transaction-manager="transactionManager"/>

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg ref="dataSource" />
    </bean>

	<jdbc:embedded-database id="dataSource" type="HSQL">
	  <jdbc:script location="script/table.sql"/>
	  <jdbc:script location="script/data.sql"/>
	</jdbc:embedded-database>

    <context:component-scan base-package="sample"/>

</beans>

TranByAnnotationMain.java (호출부분)

package sample;
import java.util.Date;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import sample.biz.domain.Pet;
import sample.biz.service.PetService;

public class TranByAnnotationMain {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("sample/config/spring-tranByAnnotation.xml");
        PetService petService = ctx.getBean(PetService.class);
        Pet pet = new Pet();
        pet.setPetId(1);
        pet.setPetName("나비");
        pet.setOwnerName("홍길동");
        pet.setPrice(10000);
        pet.setBirthDate(new Date());
        
        petService.updatePet(pet);

    }
}

 


domain - Owner.java , Pet.java

package sample.biz.domain;

import java.util.ArrayList;
import java.util.List;

public class Owner {

    private String ownerName;
    private List<Pet> petList = new ArrayList<Pet>();
    public String getOwnerName() {
        return ownerName;
    }
    public void setOwnerName(String ownerName) {
        this.ownerName = ownerName;
    }
    public List<Pet> getPetList() {
        return petList;
    }
    public void setPetList(List<Pet> petList) {
        this.petList = petList;
    }
    
    
    
}
package sample.biz.domain;

import java.util.Date;



public class Pet {

	private int petId;
	private String petName;
	private String ownerName;
	private int price;
	private Date birthDate;
	
	public Date getBirthDate() {
        return birthDate;
    }

    public void setBirthDate(Date birthDate) {
        this.birthDate = birthDate;
    }

    public int getPrice() {
		return price;
	}

	public void setPrice(int price) {
		this.price = price;
	}

	public Pet() {}
	
	public Pet(int petId, String petName) {
		this.petId = petId;
		this.petName = petName;
	}
	
	public int getPetId() {
		return petId;
	}
	public void setPetId(int petId) {
		this.petId = petId;
	}
	public String getPetName() {
		return petName;
	}
	public void setPetName(String petName) {
		this.petName = petName;
	}
	public String getOwnerName() {
		return ownerName;
	}
	public void setOwnerName(String ownerName) {
		this.ownerName = ownerName;
	}
	
	
	
}

 

dao  - PetDao.java

package sample.biz.dao;

import sample.biz.domain.Pet;

public interface PetDao {

    void updatePet(Pet pet);
}

 

exception - BussinessException.java

package sample.biz.exception;

public class BussinessException extends RuntimeException {

    
}

 

service - PetService.java

package sample.biz.service;

import sample.biz.domain.Pet;

public interface PetService {
    void updatePet(Pet pet); 
    void updatePetProgrammaticTransaction(Pet pet); 
    void updatePetProgrammaticTransaction2(Pet pet); 
}

impl - PetServiceImpl.java

package sample.biz.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import sample.biz.dao.PetDao;
import sample.biz.domain.Pet;
import sample.biz.exception.BussinessException;
import sample.biz.service.PetService;

@Service
public class PetServiceImpl implements PetService {

    @Autowired
    private PetDao petDao;

    
    
    //명시적 트랜잭션
    //호출할 메서드는 트랜잭션 매니저 인터페이스의 PlatformTransactionManage에 있다.
    //Bean정의 파일에 트랜잭션이 등록됐을 것이므로 트랜잭션 매니저의 Bean을처리하는 Bean에 인잭션 하면 된다.
    @Autowired
    private PlatformTransactionManager txManager;

    //어노테이션을 이용한 트랜잭션 처리
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, timeout = 10, readOnly = false, rollbackFor = BussinessException.class)
    public void updatePet(Pet pet) throws BussinessException {
        petDao.updatePet(pet);
    }

    public void updatePetProgrammaticTransaction(Pet pet) {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        def.setTimeout(10);
        def.setReadOnly(false);
        TransactionStatus status = txManager.getTransaction(def);
        try {
            petDao.updatePet(pet);
        } catch (RuntimeException e) {
            txManager.rollback(status);
            throw e;
        }
        txManager.commit(status);
    }

    //명시적 트랜잭션 사용
    public void updatePetProgrammaticTransaction2(final Pet pet) {
    	
    	//트랜잭션 설정
        TransactionTemplate t = new TransactionTemplate(txManager);
        t.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        t.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        t.setTimeout(10);
        t.setReadOnly(false);

        t.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                petDao.updatePet(pet);
            }
        });
    }

}

 

스프링 트랜잭션 기능에는 트랜잭션 매니저의 구현 클래스의 로그레벨을 DEBUG로 하면 로그를 출력할 수있다. 예를 들어 DataSourceTransactionManager를 사용할 때 , Log4j의 설정파일에 다음과 같은 코드를 추가하면된다.

log4j.logger.org.springframework.jdbc.datasource.DataSourceTransactionManager = DEBUG

 

파일구조

예제파일

transaction.zip
0.06MB

 

 

 

참고: 스프링 4입문 (한빛미디어)

'웹개발 > 스프링' 카테고리의 다른 글

스프링 4 MVC - part 2  (0) 2020.11.11
스프링 4 MVC - part 1  (0) 2020.11.10
스프링 4 DAO 구현  (1) 2020.11.08
스프링 4 AOP  (0) 2020.11.08
스프링 4 Bean Config  (0) 2020.11.07