상세 컨텐츠

본문 제목

[스프링 1] 8장. DB 연동(2)

22-23/22-23 Spring 1

by YUZ 유즈 2022. 11. 17. 10:00

본문

728x90

해당 포스트는 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문 [최범균 저] 책 내용을 참고하였습니다.


❗ MemberDao 테스트

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class MainForMemberDao {
	private static MemberDao memberDao;

	public static void main(String[] args) {
    		// AppCtx 설정을 사용해서 스프링 컨테이너 생성
		AnnotationConfigApplicationContext ctx = 
				new AnnotationConfigApplicationContext(AppCtx.class); 

		// 컨테이너로부터 memberDao 빈을 구해서 정적 필드 memberDao에 할당
		memberDao = ctx.getBean(MemberDao.class);

		selectAll();
		updateMember();
		insertMember();

		ctx.close();
	}

	private static void selectAll() {
		System.out.println("----- selectAll");
		int total = memberDao.count(); // 전체 행의 개수 구함
		System.out.println("전체 데이터: " + total);
		List<Member> members = memberDao.selectAll(); // 전체 Member 데이터 구함
		for (Member m : members) {
			System.out.println(m.getId() + ":" + m.getEmail() + ":" + m.getName());
		}
	}

	private static void updateMember() {
    		// email 칼럼 값이 madvirus@madvirus.net 인 Member 객체 구함
		System.out.println("----- updateMember");
		Member member = memberDao.selectByEmail("madvirus@madvirus.net"); 
		String oldPw = member.getPassword();
		String newPw = Double.toHexString(Math.random()); // 임의의 새로운 암호 생성
		member.changePassword(oldPw, newPw); // 새로운 암호 설정

		memberDao.update(member); // DB에 반영
		System.out.println("암호 변경: " + oldPw + " > " + newPw);
	}

	private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMddHHmmss");

	private static void insertMember() {
		System.out.println("----- insertMember");
		String prefix = formatter.format(LocalDateTime.now());
		// 새로 추가할 Member 객체 생성
        	// 기존 객체와 신규 객체를 구분하기 위해 현재시간을 "MMddHHmm" 형식으로 변환한 문자열을 이메일, 암호, 이름에 사용
        	Member member = new Member(prefix + "@test.com", prefix, prefix, LocalDateTime.now()); 
		memberDao.insert(member); // DB에 새로운 데이터 추가
        	// 새로 생성한 member의 키 값 출력
        	// MemberDao에서 KeyHolder로 구한 키 값을 Id로 설정해주어서 출력 가능
		System.out.println(member.getId() + " 데이터 추가"); 
	}

}

❗ 스프링의 익셉션 변환 처리

MySQL 환경에서 SQL 문법이 잘못됐을 때 MySQLSyntaxErrorException의 발생으로 BadSqlGrammarException 이 발생한다.
MySQLSyntaxErrorException은 SQLException을 상속받고 BadSqlGrammarException은 DataAccessException을 상속받는다.
JdbcTemplate의 update() 메서드는 DB 연동을 위해 JDBC API를 사용하는데, JDBC API를 사용하는 과정에서 SQLException이 발생하면 DataAccessException으로 변환해서 발생시킨다.


이런식으로 익셉션을 변환해서 재발생한다.

try {
	... JDBC 사용 코드
} catch(SQLException ex) {
	throw convertSqlToDataException(ex);
}


그럼 스프링은 왜 SQLException을 그대로 전파하지 않고 DataAccessException으로 변환하는가?

 

📌 다형성

주된 이유는 연동 기술에 상관없이 동일하게 익셉션을 처리할 수 있기 위함이다.


스프링은 JDBC, JPA, 하이버네이트 등에 대한 연동을 지원하고 MyBatis는 자체적으로 스프링 연동 기능을 제공한다. 
그런데 각각의 구현 기술마다 익셉션이 다르게 발생한다.
JDBC라면 SQLException, 하이버네니트라면 HibernateException, JPA라면 PersistenceException 이런식으로 말이다.

 

각각 익셉션을 따로 처리하지 않고 스프링이 제공하는 익셉션으로 변환함으로써 구현 기술에 상관없이 동일한 코드로 익셉션 처리할 수 있도록 한다.
 
BadSqlGrammarException 말고도 스프링은 DataAccessException을 상속한 다양한 익셉션 클래스를 제공한다.
또한, DataAccessException은 RuntimeException을 상속받는다.


❗ 트랜잭션 처리

두 개 이상의 쿼리를 한 작업으로 실행해야할 때 트랜잭션(transaction)을 사용한다.
트랜잭션은 여러 쿼리를 논리적으로 하나의 작업으로 묶어준다.

  • 트랜잭션에 쿼리 중 하나라도 실패하면, 전체 쿼리를 실패로 간주하고 실패 이전의 기존 상태로 돌리는 것 → 롤백(rollback)
  • 트랜잭션에 모든 쿼리가 성공해서 쿼리 결과를 DB에 실제로 반영 → 커밋(commit)

스프링은 트랜잭션 기능 또한 제공한다.

 

📌 @Transactional을 이용한 트랜잭션 처리

@Transactional 애노테이션은 스프링이 제공하는 트랜잭션 범위를 지정하는 애노테이션이다.
트랜잭션 범위에서 실행하고 싶은 메서드에 @Transactional만 붙이면 쉽게 지정할 수 있다.

 

@Transactional 애노테이션이 제대로 동작하려면 다음 두가지를 스프링 설정(@Configuration)에 추가해야한다.

  • PlatformTransactionManager 빈 설정
  • @Transactional 애노테이션 활성화 설정
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;


@Configuration
@EnableTransactionManagement //!
public class AppCtx {

	@Bean(destroyMethod = "close")
	public DataSource dataSource() {
		DataSource ds = new DataSource();
		ds.setDriverClassName("com.mysql.jdbc.Driver");
		ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8");
		...
		return ds;
	}

	@Bean
	public PlatformTransactionManager transactionManager() { //!
    		// JDBC는 DataSourceTransactionManager 클래스를 PlatformTransactionManager로 사용
		DataSourceTransactionManager tm = new DataSourceTransactionManager();
        	// dataSource 프로퍼티로 트랜잭션 연동에 사용할 DataSource를 지정
		tm.setDataSource(dataSource());
		return tm;
	}


	@Bean
	public MemberDao memberDao() {
		return new MemberDao(dataSource());
	}
}


PlatformTransactionManager 는 스프링이 제공하는 트랜잭션 매니저 인터페이스이다.
스프링은 구현 기술에 상관없이 동일한 방식으로 트랜잭션을 처리하기 위해 인터페이스를 사용한다.

@EnableTransactionalManagement 애노테이션은 @Transactional이 붙은 메서드를 트랜잭션 범위에서 실행하는 기능을 활성화한다.
즉, @Transactional 활성화를 설정한다.

 

등록된 PlatformTransactionManager 빈을 사용해서 트랜잭션을 적용한다.

위와 같이 설정 후, 트랜잭션 범위에서 실행하고 싶은 스프링 빈 객체의 메서드에 @Transactional 애노테이션을 붙이면 된다.

트랜잭션이 실제로 시작되고 커밋되는지 확인하고 싶다면 트랜잭션과 관련된 로그 메세지를 출력하는 Logback을 사용한다.

 

📌 @Transactional과 프록시

그럼 트랜잭션을 시작하고, 커밋하고, 롤백하는 것은 누가 어떻게 처리하는걸까?
내부적으로 AOP를 사용한다. 즉, 트랜잭션 처리는 프록시를 통해서 이루어진다.

@Transactional 애노테이션을 적용하기 위해 @EnableTransactionManagement 태그를 사용하면 스프링은 @Transactional이 적용된 빈 객체를 찾아서 알맞은 프록시 객체를 생성한다.

 

getBean()을 실행하면 실제 객체 대신에 트랜잭션 처리를 위해 생성한 프록시 객체를 리턴한다. 

이 프록시 객체는 @Transactional 애노테이션이 붙은 메서드를 호출하면 PlatformTransactionManager를 사용해서 트랜잭션을 시작한다.


트랜잭션을 시작한 후 실제 객체의 메서드를 호출하고, 성공적으로 실행되면 트랜잭션을 커밋한다.

 

📌 롤백(rollback) 처리

롤백 또한 프록시 객체가 처리한다.

@Transactional을 처리하기 위한 프록시 객체는 원본 객체의 메서드를 실행하는 과정에서 RuntimeException이 발생하면 트랜잭션을 롤백한다.
책에서 구현한 WrongIdPasswordException 클래스가 RuntimeException을 상속하는 이유는 트랜잭션 롤백을 염두해 두었기 때문이다. 
JdbcTemplate에서 DB 연동 과정에 문제가 있으면 DataAccessException을 발생하는데, 이 역시 RuntimeException을 상속받고 있어서 도중 익센셥이 발생해도 프록시는 트랜잭션을 롤백할 수 있다.


SQLException은 RuntimeException을 상속하고 있지 않으므로 발생해도 트랜잭션을 롤백하지않는다.

RuntimeException 뿐만아니라 SQLException이나 IOException이 발생했을 때 롤백하고 싶다면 @Transactional의 rollbackFor 속성을 사용하면 된다.

@Transactional(rollbackFor={SQLException.class, IOException.class})
public void someMethod() {
	...
}


noRollbackFor 속성은 반대로 지정한 익셉션이 발생해도 롤백시키지 않고 커밋할 익셉션 타입을 지정한다.

 

📌 트랜잭션 전파

@Transactional 애노테이션의 주요 속성 중 하나인 propagation의 열거 타입 목록 중 기본값인 REQUIRED 값

  • 메서드를 수행하는 데 트랜잭션이 필요하다는 것을 의미한다. 현재 진행 중인 트랜잭션이 존재하면 해당 트랜잭션을 사용한다. 존재하지 않으면 새로운 트랜잭션을 생성한다.

이 설명을 이해하기 위해 트랜잭션 전파에 대해 알아보자.

public class SomeService {
	private AnyService anyService;
	
	@Transactional
	public void some() {
		anyService.any()
	}

	public void setAnyService(AnyService as) {
		this.anyService = as;
	}
}

public class AnyService {
	@Transactional
	public void any() { ... }
}
@Configuration
@EnableTransactionManagement
public class Config {
	@Bean
	public SomeService some() {
		SomeService some = new SomeService();
		some.setAnySercie(any());
		return some;
	}

	@Bean
	public AnyService any() {
		return new AnyService();
	}

	// DataSourceTransationManager 빈 설정
	// DataSource 설정
}

SomeService 클래스와 AnyService 클래스는 둘 다 @Transactional 을 적용했다.
SomeService의 some() 메서드를 호출하면 트랜잭션이 시작되고 AnyService의 any() 메서드를 호출해도 트랜잭션이 시작된다. 
그렇다면 어떻게 트랜잭션 처리가 될까? 

 

@Transactional의 propagation 속성의 기본값은 Propagation.REQUIRED 이므로 처음 some() 메서드를 호출할 때 트랜잭션을 새로 시작한다.
some() 메서드 내부에서 any() 메서드를 호출할 때는 이미 some() 메서드에 의해 시작된 트랜잭션이 존재하므로 트랜잭션을 새로 생성하지 않고 존재하는 트랜잭션을 그대로 사용한다.
즉, some() 메서드와 any() 메서드를 한 트랜잭션으로 묶어서 실행한다.

만약 any() 메서드에서 적용한 @Transactional의 propagation 속성값이 REQUIRES_NEW라면, 아래 설명 처럼 some() 메서드에 의해 트랜잭션이 생성되고 다시 any() 메서드로 트랜잭션이 생성된다.

  • 항상 새로운 트랜잭션을 시작한다. 진행 중인 트랜잭션이 존재하면 기존 트랜잭션을 일시 중지하고 새로운 트랜잭션을 시작한다. 새로 시작된 트랜잭션이 종료된 뒤에 기존 트랜잭션이 계속된다.

 

@Transactional 애노테이션이 적용되어있지 않은 메서드가 트랜잭션 도중 호출될 때는 JdbcTemplate 클래스 덕에 트랜잭션 범위에서 쿼리를 실행할 수 있다.
JdbcTemplate은 진행 중인 트랜잭션이 존재하면 해당 트랜잭션 범위에서 쿼리를 실행한다.


Spring 1

EDITOR: OJO

728x90

관련글 더보기