게시판 만들기를 하면서 기계처럼 붙이고 보는 @Transactional을 왜 사용하는지 머릿속에 정리가 안 돼서 다시 강의를 보면서 정리를 해봤다.

 

데이터베이스를 사용하는 중요한 이유는 하나는 트랜잭션이라는 개념을 지원하기 때문인데 트랜잭션이란 하나의 논리적인 작업 단위로 계좌이체로 예를 들면 돈을 보내고 받는 과정이 하나의 작업이 되는 것이다. 작업이 성공적으로 끝나면 커밋(commit), 중간에 실패하면 되돌리는 롤백을 한다.

 

트랜잭션은 ACID를 보장해야 하는데 이 중에서 격리성(Isolation)은 동시성의 정도를 나타내는데 동시성을 보장하는 것은 성능과의 trade-off가 있어서 격리 수준이 나뉘게 된다.

 

 

 

[DB 접근 기술1] 트랜젝션(Transaction) 기초

트랜젝션 데이터를 저장할 때 단순 파일이 아닌 데이터베이스에 저장하는 가장 큰 이유는 데이터베이스가 트랜젝션이라는 개념을 지원하기 때문이다. 트랜젝션은 데이터베이스에서 하나의 거

treecode.tistory.com

 

[Real MySQL] MySQL의 격리 수준

트랜잭션의 격리 수준이란 여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것이다. 격리 수준 READ U

treecode.tistory.com

 

트랜잭션을 사용하기 위해서는 auto commit을 false로 설정하면 되는데 일반적으로는 디폴트 값이 true로 되어 있다. 이렇게 되면 매번  쿼리마다 자동으로 커밋이 되다 보니 하나의 작업 단위를 묶을 수가 없어서 auto commit 옵션을 false로 주고 개발자가 직접 commit, rollback을 해야 된다.

 

커넥션 연결을 하게 되면 데이터베이스 내부에 세션이 생성되고 세션을 통해서 트랜잭션을 시작하고 SQL을 실행, 커밋, 롤백, 트랜잭션 종료와 같은 작업이 이루어진다.

하나의 논리적인 작업 단위의 기준은 비즈니스 로직으로 서비스 계층에서 시작을 해야 하는데 이렇게 되면 서비스 클래스에서 커넥션을 꺼내기 위해 DataSource를 가지고 트랜잭션 시작, commit, rollback, 트랜잭션 종료 등의 처리를 하는 코드가 들어가게 된다.

 

public void accountTransfer(String fromId, String toId, int money) throws
  SQLException {
	Connection con = dataSource.getConnection();
	try {
        con.setAutoCommit(false); //트랜잭션 시작 //비즈니스 로직
        bizLogic(con, fromId, toId, money); con.commit(); //성공시 커밋
    } catch (Exception e) { con.rollback(); //실패시 롤백
        throw new IllegalStateException(e);
    } finally {
        release(con);
    }
}

위의 예시에서 bizLogic()에서는 트랜잭션을 유지하기 위해 동일 커넥션을 사용해야 되고 repository를 호출할 때마다 커넥션을 파라미터로 전달해줘야 한다. 그리고 SQLException 같이 JDBC 전용 예외 같은 구체화에 의존성이 생기는 등 JdbcTempalte, JPA .. 데이터 접근 기술의 변경에 영향을 받게 된다.

 

스프링은 이러한 문제를 해결하기 위해 트랜잭션 추상화 인터페이스 TransactionManager를 제공하는데 트랜잭션 추상화, 커넥션 동기화 역할을 대신해 준다. 

 

 

커넥션은 트랜잭션 동기화 매니저가 스레드별 저장소인 스레드 로컬을 통해 동기화를 해서 파라미터로 전달하지 않아도 유지할 수 있다. 스레드 로컬은 사용 후 정리하는 것이 매우 중요하기 때문에 (+ 커넥션 반납 등) 개발자가 직접 하기보다는 제공되는 기술을 사용하는 것이 안전하다.

 

트랜잭션 매니저를 사용해도 커밋, 롤백 try-catch를 하는 코드가 남아있는데 스프링 AOP를 활용한 @Transactional을 사용하면 깔끔하게 처리가 가능해진다.

 

@Transactional을 정확히 이해하려면 스프링 AOP를 학습해야 되는데 일단은 왜 써야 하는지 정도만 알고 넘어갔다.

@Transactional을 클래스에 메서드에 사용하면 트랜잭션 AOP는 프록시를 만들어 스프링 컨테이너에 등록하는데 이 프록시가 트랜잭션을 관리하는 로직을 대신 처리해 주게 된다. 

 

한 가지 주의점은 외부에서 요청을 하면 프록시 객체가 요청을 먼저 받아서 트랜잭션을 처리하고, 실제 메서드를 호출해 주는데 만약 내부에서 메서드를 호출하는 경우 프록시를 거치지 않아서 트랜잭션이 적용되지 않는 문제가 생긴다.

이전에는 클래스에 @Transactional을 readOnly 설정으로 두고 데이터 삽입, 수정, 삭제 메서드만 따로 @Transacional을 선언해서 사용을 해서 그런지 이런 위험 요소를 인식하지 못했다.

 

클래스에 @Transactional을 선언하는 경우 트랜잭션이 의도하지 않은 곳까지 과도하게 적용이 돼서 메서드 단위로 적용을 한다. 트랜잭션은 주로 비즈니스 로직의 시작점에 걸기 때문에 외부에서 열어둔 곳을 시작점으로 해서 클래스에 @Transactional 애노테이션을 선언해도 public 메서드에만 적용이 된다.

 

 

추가적으로 스프링 트랜잭션 AOP는 예외의 종류에 따라 트랜잭션을 커밋하거나 롤백하는데 RuntimeException , Error와 그 하위 예외가 발생하면 롤백, 체크 예외와 그 하위 예외가 발생하면 커밋을 한다. (@Transactional에서 rollbackFor 옵션으로 변경 가능)

체크 예외의 경우 비즈니스적인 예외 상황에서 주로 사용한다고 하는데 예를 들면 회원의 주문 Order에서 잔고 부족의 경우 일단 커밋을 하고 추가 결제를 하도록 할 수도 있다. (생각해 보니 체크 예외는 사용한 적이 없는 거 같은데 중요한 비즈니스 예외 상황에서 사용하면 좋을 것 같다.)

 

간단 정리

@Transactional 사용하는 이유

 

- 애플리케이션에서 트랜잭션 관리

- 비즈니스 로직과 관련 없는 중복 코드 제거

- 예외 전파, 관리

 

JdbcTemplate은 템플릿 콜백 패턴을 사용해서 JDBC를 직접 사용할 때 발생하는 대부분의 반복 작업을 대신 처리해 준다.

 

- 커넥션 획득, 종료, statement, resultset 종료

- 트랜잭션을 다루기 위한 커넥션 동기화

- 예외 발생 시 스프링 예외로 변환

 

JdbcTemplate에서 tryForStream() 메서드를 사용하면 예외적으로 직접 리소스 종료를 해줘야 하는데 그렇지 않을 경우 커넥션 반납이 되지 않는 문제가 생긴다. 같이 공부하는 분이 커넥션 반납이 되지 않는다고 물어보셨을 때 @Transactional 역할을 잘 알고 있었으면 이 부분을 먼저 확인해 봤을 거 같은데 금붕어처럼 다 까먹고 말똥말똥 쳐다보고 있었다. @Transactional을 사용하면 트랜잭션 기능 외에도 DB 접근 기술마다 커넥션이 종료되지 않는 상황에서 안전하게 처리를 해주는 이점도 있는 것 같다.

 

 

[참고]

인프런 김영한님 DB 접근 기술 1,2편

디자인 패턴은 소프트웨어 설계 시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용할 수 있는 재사용 가능한 솔루션으로 디자인 패턴에서 중요한 것은 각 패턴의 핵심이 담긴 목적 또는 의도이다.

 

1. 패턴을 적용할 상황

2. 해결해야 될 문제

3. 핵심 의도가 무엇인지

 

 

메서드를 추상 메서드(abstaract method)로 선언해서 자식 클래스가 이를 상속 받아 메서드를 구현하도록 하면 클래스 계층구조를 통해 관심을 분리할 수 있다. (DB의 커넥션 연결 등과 SQL, DB 접근 등의 로직을 분리)

 

상속을 통해 기능을 확장하는 방법으로 사용되는 2가지 패턴이 있는데 먼저 템플릿 메서드 패턴(template method pattern)은 부모 클래스에서 자주 변경되거나 확장할 기능을 추상 메서드나 오버라이드 가능한 메서드를 정의해둔 템플릿 메서드를 만들어서 자식 클래스에서 해당 메서드를 구현하도록 하는 패턴이다. (훅(Hook) 메서드 : 부모 클래스에서 디폴트 기능을 정의해두거나 비워뒀다가 자식 클래스에서 오버라이드할 수 있도록 만들어둔 메서드)

팩토리 메서드 패턴(factory method pattern)자식 클래스에서 객체 생성 방법과 클래스를 결정할 수 있도록 미리 정의해둔 메서드(팩토리 메소드)를 통해 객체의 생성을 부모 클래스의 기본 코드에서 독립시키는 패턴이다.

 

관심을 분리한다는 것은 내부 동작에 상관없이 관심을 두지 않고 필요한 기능만 가져다 사용한다는 것이다. 

 

템플릿 메서드 패턴, 팩토리 메서드 패턴을 사용하면 관심 사항이 다른 코드를 분리해내고, 서로 독립적으로 변경, 확장을 할 수 있다. 하지만 상속을 사용하기 때문에 클래스의 다중 상속이 허용되지 않는 자바에서는 비용이 큰 편이고, 결합력이 높기 때문에 관심 사항은 분리했더라도 변경 시 변화의 파급력이 큰 편이다. 그래서 토비의 스프링 책에서는 인터페이스를 사용하여 분리를 하는 방식을 권장하는데 인터페이스를 사용하면 다형성으로 인해 의존관계를 외부에서 주입하고 클래스에서는 인터페이스 타입의 참조 변수를 통해 구현체의 메서드를 사용하면 되기 때문에 완벽하게 분리를 할 수 있다. 구현체가 변경이 되더라도 외부에서 변경된 구현체로 주입을 해주면 된다.

 

이렇게 보면 인터페이스를 사용하면 될 것 같은데 템플릿 메서드 패턴을 왜 쓰는지, 어떤 상황에 쓰이는지 잘 모르겠다. 다른 블로그를 찾아보다보니 템플릿 메서드 패턴과 비슷하면서 상속의 단점을 제거할 수 있는 전략 패턴이 있다고 하는데 인프런 김영한님의 스프링 고급편에도 템플릿 메서드 패턴에 대한 목차가 있어서 강의를 통해 더 깊게 공부해보고 코드로 사용해보면서 정리를 해봐야겠다.

 

 

 

 

[참고]

토비의 스프링

 

Overriding in Java - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

 

 

팩토리 메서드, 템플릿 메서드 패턴

1. 팩토리 메서드 패턴 팩토리 메서드 패턴(Factory Method Pattern)이란 객체 생성을 하는 클래스를 따로 두는 것입니다. 그래서 공장(Factory)이라는 표현을 쓰나봅니다. 🙂 실질적인 클래스의 구현은

western-sky.tistory.com

 

 

Template Method (템플릿 메서드) 패턴

1. Overview 코드를 작성하다보면 로깅, 예외 처리 등등 반복되어 작성하는 코드가 발생합니다. 이런 경우 코드의 중복을 없애기 위한 패턴 중 하나가 템플릿 메서드 패턴입니다. AbstractClass (추상

bcp0109.tistory.com

 

이전에는 스프링 시큐리티 아키텍쳐에 따라 어떤식으로 동작이 되는지 보았다.

 

 

[Spring] 스프링 시큐리티(Spring Security) + JWT 토큰 구현(1)

스프링 시큐리티는 스프링 기반 애플리케이션의 인증, 보안 등을 처리하기 사용되는 스프링 하위 프레임워크로  권한을 부여하여 접근을 제한하고 로그인 유지 등의 용도로 사용이 된다. 스프

treecode.tistory.com

 

스프링 시큐리티를 사용하기 위해서는 Security Configuration 설정 클래스를 만들어야 되는데 이전에는 WebSecurityConfigureAdapter를 extends해서 configure 메소드를 오버라이드 했지만 최근에 스프링 버전이 바뀌면서 WebSecurityConfigureAdapter가 Deprecated 되었다. 그래서 SecurityFilterChain을 Bean으로 등록해서 설정해야 한다.

 

1번을 보면  @EnableWebSecurity 애노테이션은 스프링 시큐리티를 사용하기 위해 필수로 적용하는 애노테이션이다.

filterChain 파라미터를 보면 HttpSecurity를 받을 수 있는 것도 @EnableWebSecurity에 HttpSecurityConfiguration이 Import되어 있기 때문에 가능하다.  

@EnableWebSecurity에 Import 되어 있는 WebSecurityConfiguration을 보면 springSecurityFilterChain() 메소드를 실행해서 Beand으로 등록한 WebSecurityCustomizer, SecurityFilterChain 등으로 WebSecurity를 생성한다.

 

WebSecurity는 performBuild() 메소드를 통해 FilterChainProxy를 만드는데 이 FilterChainProxy는 DelegatiingFilterProxy로부터 위임을 받아 실제 처리를 담당하는 스프링 시큐리티의 필터의 핵심 클래스이다.

 

DelegatingFilterProxy서블릿 컨테이너와 스프링 컨테이너 사이를 연결하는 서블릿 필터로  Http Request 요청이 오면 FilterChainProxy로 위임(Delegate)을 해서 스프링 시큐리티의 기본 필터와 커스텀 필터를 거치게 된다.

 

아래 그림을 보면 Request가 서블릿에 도달하기 전 SecurityFilterChain이 적용되는 것을 볼 수 있는데 서블릿 필터에서 스프링 빈 필터를 주입 받아 사용할 수 없기 때문에 중간 다리 역할을 해주는 스프링 시큐리티의 특수한 필터인 FilterChainProxy에게 위임을 하는 것이다.

 

 

 

 

 

 

@EnableGlobalMethodSecurity는 컨트롤러에 @PreAuthorize 애노테이션을 사용하여 권한에 따라 접근할 수 있도록 하기 위해 선언한다.

 

 

2번에서는 예외를 핸들링하기 위해  AccessDeniedHandler, AuthenticationEntryPoint를 추가하였고 커스텀 AuthenticationProvider를 주입하였다. 커스텀 AuthenticationProvider는 따로 코드로 자세히 보겠지만 Jwt 토큰을 인증하고 발급하는 등의 처리를 담당한다. 4번, 7번에서 각각 적용하는 것을 볼 수 있다.

 

 마지막으로 3번, 6번을 보면 특정 리소스를 무시하고 어떠한 요청에 인가(authorization)을 해주는지에 대한 설정을 하는 것이고 8번의 PasswordEncoder는 비밀번호를 암호화하기 위해 BcryptPasswordEncoder를 빈으로 등록하였다.

 

다음에는 JWT 토큰에 대해 자제시 알아보고 7번에서 JwtFilter에  jwtAuthenticationProvider를 주입해서 필터로 등록하는 것에 대해 공부한다. 

 

 

[참고]

https://oh-sh-2134.tistory.com/114

https://uchupura.tistory.com/24

https://velog.io/@jeongyunsung/스프링부트-해부학-Security1-

스프링 시큐리티스프링 기반 애플리케이션의 인증, 보안 등을 처리하기 사용되는 스프링 하위 프레임워크로  권한을 부여하여 접근을 제한하고 로그인 유지 등의 용도로 사용이 된다.

 

 

스프링 시큐리티의 기본 동작은 컨트롤러를 통해 유저 정보를 담은 Http Request 요청이 오면 AuthenticationFilter에서 인증 전 상태의 UsernamePasswordAutienticationToken을 생성한다.

UsernamePasswordAutienticationToken은 Authentication 인터페이스를 implements한 AbstractAuthenticationToken을 상속 받은 클래스로 두개의 생성자를 보면 인증 전, 인증 후 토큰을 생성하는 것을 알 수 있다. (principal : 아이디, credentials : 비밀번호)

 

 

해당 토큰으로 AuthenticationManagerBuilder를 통해 AuthenticationManager에서 authenticate() 메서드를 실행하면 내부적으로 AuthenticationManager의 구현체인 ProviderManager가 동작한다.

 

ProviderManager는 DaoAuthenticationProvider를 호출한다. (DaoAuthenticationProvider는 AuthenticationProvider의 추상 클래스인 AbstractUserDetailsAuthenticationProvider를 상속 받음)

DaoAuthenticationProvider의 retrieveUser() 메소드를 보면  UserDetailsService의 loadByUsername() 메서드를 실행하는 것을 볼 수 있다.

 

 

UserDetailsService는 커스텀으로 구현하는데 loadUserByUsername을 오버라이드해서 파라미터로 넘어온 Username으로 조회를 하고 존재할 경우 UserDetails 객체의 인스턴스를 생성해서 반환한다.

유저 정보가 인증이 되었으면 UserDetails 인스턴스가 반환이 되고 다시 DaoAuthenticationProvider로 돌아와서 비밀번호(Credential)가 일치하는지 확인을 한다. 인증 전 로그인 정보의 비밀번호는 암호화가 되어있지 않지만 내부적으로 passwordEncoder가 작동한다.

 

정리를 하자면 ProviderManager에서 authenticate()가 실행이 되면 DaoAuthenticationProvider에서 실질적인 인증 처리를 한다.

 

1) 유저 정보를 확인하기 위해 retrieveUser()가 실행, UserDetailsService의 loadUserByUsername()를 통해 UserDetails 반환

2) 비밀번호를 확인하기 위해 additionalAuthenticationChecks()가 실행, 반환 받은 UserDetails 정보로 비밀번호 검증

 

이렇게 인증이 완료된 Authentication 인스턴스를 SecurityContext에 저장을 하고 SecurityContextHolder에 넣는다.

스프링 시큐리티를 세션 방식으로 구현할 때 SecurityContext에 인증이 완료된 Authentication 인스턴스를 저장하고 이를SecurityContextHolder에 담아 보관을 하는데 JWT 토큰으로 구현을 하더라도 똑같이 저장을 해야 한다.

SecurityContext에 Authentication 객체를 저장하는 이유는 세션과 상관 없이 ThreadLocal 때문이다. SecurityContextHolder는 ThreadLocal을 가지고 있는데 ThreadLocal은 세션의 범위가 아닌 Request 범위에 속한다. SecurityContext에 Authentication 객체가 저장되어 있지 않으면 FilterSecurityIntercepter에서 필터에 걸리게 된다.

(SecurityContextHolder와 ThreadLocal에 대한 부분은 좀 더 공부를 해봐야 알 것 같다.)

https://www.inflearn.com/questions/501092

https://www.inflearn.com/questions/558844

 

 

[참고]

 

https://bcp0109.tistory.com/301

https://mangkyu.tistory.com/76

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-jwt

 

+ Recent posts