Spring Boot를 이용해서 개발을 진행하면 @Transactional이란 어노테이션을 이용하여 데이터의 영속성을 지키고자 한다. 제대로 알고 사용하는 것일까? 프록시조차 모르는 나는 Spring Boot의 의도에 대로 비즈니스 로직에만 신경을 쓰는 것이었을까? 오늘은 그저 사용 중이었던 @Transactional에 대한 내 생각을 서술한 글을 쓴다.
@Transactional을 제대로 이해하려면 유튜브 뉴렉처 강의가 직관적이었다. Spring 자체를 이용해본 적이 없기 때문에 Bean을 xml에 직접적으로, 프록시를 직접적으로 설정해서 AOP를 구현하는 것을 본 것이 이해가 잘 가는 부분이었다.
문제는 그 다음이다. Spring Boot는 AspectJ 라이브러리를 이용한 것이 아니라, 컴파일 단계에 AOP를 구현한 것이라 Spring Aop 라이브러리는 프록시를 이용하였기에 Self-invocation이란 문제가 발생한다.
해당 문제는 자기 자신의 메소드를 호출할 때 AOP가 제대로 적용되지 않는 문제가 발생한다.
A 메소드에서 @Tranascational이 붙은 B를 호출하면 @Transcational이 적용이 안 된다. 하지만 A가 @Transcaional이 붙어 있으면 제대로 적용된다.
해당 문제를 보면서 서비스를 하나 더 생성하여 해당 문제를 해결하고자 하였는데, 생각해보니 @Transcaional이 붙은 B는 insert 만 실행하는 코드가 하나만 있다. 그렇기에 해당 메소드에는 어노테이션이 필요없겠다는 생각이 들었다. Rollback할 요소가 없기 때문이다. Insert가 실패하면 어차피 데이터가 삽입되지 않기 때문이다.
문제는 반대로 A에서 @Transcational을 붙여서 해결할 수도 있는데 이때 DB Pool을 잡아 먹는 것인지는 더 알아볼만한 요소라 찾고 이후 글을 하나 더 작성하려고 한다.
여기서 th:inline을 사용하면 js 변수 내에 [[${ }]]로 가능하다. 이게 왜 좋냐면 th:inline을 사용하지 않으면 [[${}]] 양 옆에 /* */를 사용해야 한다. 그리고 외부 Js에서는 이렇게 사용할 수가 없다. 그런 이유는 html을 렌더링? 서버에서 템플릿 작업을 해주면서 템플릿 언어에 해당하는 문법을 보고 작업을 해준다.
그렇기 때문에 html에 외부 js 파일이 내장되어 있지 않으니 정상적으로 변수에 값이 들어가지 않는 것이다.
이렇게 라이브러리를 단순히 추가함으로 왼쪽(추가 전)에 되던 통신이 오른쪽처럼 401로 막히게 됩니다. 정확하지는 않지만, 실제 코드 실행 시 막히는 현상을 파악했습니다.
TIP ^V^
401은 사용자가 로그인이 되어 있지 않은 상태 => 인증 실패 (개발자는 앱 설계 시 자격 증명이 누락되거나 잘못되었을 때 ) 403은 사용자가 권한이 없는 상태 => 권한 부여 실패, 호출자를 식별하였지만, 이용 권리 없음을 나타냄
2. 스프링 시큐리티 인증 절차 설명
조금 복잡하게 되어있지만 하이라이트만 확인해보면 좋겠습니다. Oauth2.0을 사용하면 위에 정의된 아키텍쳐에 들어가는 구성 요소의 이름이 조금 바뀝니다.
1. 인증 필터 (Authentication Filter): 인증 요청을 인증 관리자에게 위임하고 응답을 바탕으로 보안 컨텍스트를 구성함 2. 인증 관리자 (Authentication Manager): 인증 공급자를 이용하여 인증을 처리한다. 3. 인증 공급자 (Authentication Provider): - 사용자 관리 책임을 구현하는 사용자 세부 정보 서비스를 인증 논리에 이용한다. - 암호 관리를 구현하는 암호 인코더를 인증 논리에 이용한다. 4. 유저 세부 정보 서비스 (User Details Service) 5. 보안 컨텍스트(Security Context): 인증 프로세스 후 인증 데이터를 유지한다.
3. 자동으로 구성되는 빈
1. UserDetailsService 2. PasswordEncoder 인증 공급자 (Authentication Provider)는 위 두 개의 빈을 이용하여 1)사용차를 찾고, 2) 암호를 확인합니다.
사용자 정보 관리하는 UserDetailsService
스프링 시큐리티에서 기본적으로 제공하는 기본 구현을 이용했지만, 우리는 입맛에 맞게 바꿔서 사용한다.
1차 정리
1. 기본적으로 UserDetailsService와 PasswordEncoder가 Basic 인증 흐름에 꼭 필요하다는 사실을 인지해야 한다. 2. Basic 인증에서는 클라이언트가 사용자 이름과 암호를 HTTP Authorization 헤더를 통해 보내기만 하면 된다. => 헤더 값에 접두사 Basic을 붙이고 그 뒤에 :으로 구분된 사용자 이름과 암호가 포함된 문자열을 Base64 인코딩하여 붙인다.
4. 문제🙄: 기존 websecurityconfigureradapter deprecated
기존에 사용하던 WebSecurityConfigureradapter가 Deprecated 되었다. 거의 대부분 인터넷이나 서적을 찾아보면 이를 사용하는 코드가 적혀있지만, 현재 기준으로 Deprecated가 되어 다른 코드를 사용해야 한다. 자세한 내용은 아래 하단 링크를 통해 확인하면 된다.
이 클래스를 생성하는 이유는 Security Context에 저장되는 인증 객체를 만들기 위함입니다. 이 클래스는 추후 컨트롤러에서 파라미터로 받아오는 객체의 클래스입니다.
먼저 코드를 살펴보면 UserDetails를 구현합니다. 코드를 확인해봅시다.
import com.example.demo.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.stream.Collectors;
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getAuthorities().stream().map(a -> new SimpleGrantedAuthority(a.getName())).collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public final User getUser() {
return user;
}
}
실제 코드를 확인해보면 위와 같은 주석이 있습니다.
Authentication 객체에 캡슐화가 된다는 부분이 중요합니다. 왜냐하면 나중에 Security Context에 넣을 때 Authentication 객체만 들어가기 때문입니다. 그래서 우리는 User 객체를 가지고 있지만 Authentication으로 캡슐화가 될 수 있는 UserDetails를 구현합니다.
Oauth2.0에 대해 작년에 아무런 지식없이 인터넷을 방황하며 코드를 작성했었다. 당시 Nestjs로 작성했었는데 이제는 다른 프레임워크로 작성해야했다. 이번에는 스프링 부트로 oauth를 이용하려고 하고 라이브러리를 찾아본 결과 시큐리티 5에 있는 oauth2.0-client를 이용했다.
어려운 부분은 크게 2가지였다.
1. 트위치 Oauth에 적용하기 2. Oauth2.0에 대한 지식
해결했던 방법
1. 트위치 Oauth2.o에 적용을 해결했던 방법
카카오와 네이버도 스프링 Oauth2.0 라이브러리에서 제공하는 기본 제공자가 아니다. 이 두개의 적용 방법과 트위치 개발자 페이지와 검색을 통해서 해결했다.
권한 코드는 백엔드에서 처리하는 방식에 적합하고 위 설정 파일에서spring.security.oauth2.client.registration.twitch.authorization-grant-type = authorization_code 라고 적은 부분도 어떤 방식을 이용하겠다고 설정한 것이다.
코드 컨밴션이 존재하지 않아, 프로젝트 내에 코드의 일관성이 일치하지 않는 현상이 발생하였다. 그 중 하나로 의존성 주입하는 방식이 거론되어, 의존성 주입 방식의 차이와 실제 오류가 발생하는지를 알아보게 되었다.
의존성 주입 방식에는 크게 3가지가 존재한다.
필드 의존성
생성자 의존성
setter 의존성
1. 필드 의존성 주입 (Field Injection)
public class A {
@Autowired
private AService aService;
}
2. setter 의존성 주입 (Setter based Injection)
public class A {
private AService aService;
@Autowired
public void setAService(AService aService){
this.aService = aService;
}
}
3. 생성자 의존성 주입 (Constructor based Injection)
// 의존성 주입 대상이 한 개일 경우, 생성자에 @Autowired 어노테이션을 붙이지 않아도 된다.
public class A {
private final AService aService;
public A(AService aService){
this.aService = aService;
}
}
// 의존성 주입 대상이 2개이상인 경우, 생성자에 @Autowired 어노테이션을 붙여야 한다.
public class A {
private final AService aService;
private final BService bService;
@Autowired
public A(AService aService, BSerivce bService){
this.aService = aService;
this.bService = vService;
}
}
B. 차이점: Bean 주입 순서
1. 필드 의존성 주입
주입 받으려는 빈의 생성자를 호출하여, 빈을 찾거나 빈 팩토리에 등록한다.
생성자 인자를 사용하는 빈을 찾거나 만든다.
필드에 주입한다.
2. Setter 의존성 주입
주입받으려는 빈의 생성자를 호출하여, 빈을찾거나 빈 팩토리에 등록한다.
생성자의 인자를 사용하는 빈을 찾거나 만든다.
주입하려는 빈 객체의 수정자(setter)를 호출하여 주입한다.
🟥 필드, Setter 의존성 방식은 런타임에서 의존성을 주입하여, 의존성을 주입하지 않아도 객체가 생성될 수 있다.
3. 생성자 의존성 주입
생성자의 인자에 사용되는 빈을 찾거나 빈 팩토리에서 만든다.
찾은 파라미터 빈으로 주입하려는 생성자를 호출한다.
🟥 객체가 생성되는 시점에 빈을 주입하여, 의존성 주입되지 않아 발생할 수 있는 NullPointerException을 방지한다.
C. 순환 참조
A에서 B를 호출하고, B에서 A를 호출하는 관계를 순환 참조라 한다. 이런 경우가 발생하는 일은 한 프로젝트 내에 관리해야하는 클래스가 많아지는 경우 실수로 발생하게 된다.
@Service
public class Aservice {
@Autowired
private Bservice bservice;
}
@Service
public class Bservice {
@Autowired
private Aservice aservice;
}
필드와 Setter 방식에서는 빈이 생성된 후에 참조를 하기 때문에 애플리케이션이 아무런 오류나 경고없이 구동된다. 실제 코드가 호출될 때까지 알 수 없다는 의미이다.
생성자를 통해 의존성 주입할 경우 BeanCurrentlyInCreationException이 발생한다. 순환참조뿐만 아니라 의존 관계에 내용을 외부로 노출 시킴으로 애플리케이션이 실행하는 시점에서 오류를 확인할 수 있다.
D. 코드의 깔끔함
1. 필드 의존성 주입
@Service
public class Aservice {
@Autowired
private Bservice bservice;
}
장점: 한 필드에 어노테이션을 붙임으로써 생성자를 만들지 않아 코드의 수가 줄어든다.
단점: 필드가 많아질 경우 한 줄씩 어노테이션이 추가적으로 붙게 된다.
2.생성자 주입과 Loombok
매번 생성자를 만들어서 주입하는 것이 코드의 깔끔함을 저해시킨다. 하지만 Lombok을 이용한다면 생성자를 자동으로 만들어준다.
@RequiredArgsConstructor
@Service
public class Aservice {
private final Bservice bservice;
}
RequiredArgsConstructor는 final로 선언된 필드를 가지고 생성자를 만들어준다.
E. final 사용
필드나 setter 방식을 이용시에 final 키워드를 사용할 수 없다.
final 키워드 사용 시 런타임 상에서 의존성이 변경되는 가능성을 제거해준다.
F. 실제 코드 실행 시 발생 상황 관찰하기
@Service
public class A {
@Autowired
B b;
}
@Service
public class B {
@Autowired
A a;
}
@Service
public class A {
private final B b;
public A(B b) {
this.b = b;
}
}
@Service
public class B {
private final A a;
public B(A a) {
this.a = a;
}
}
오류가 발생하지 않았다.
앞에서 한 말이 거짓말일까?
2.6.0 버전 특징
기본적으로 순환참조가 발생하지 않는다.
spring.main.allow-circular-references=true
필드 주입 = 속성 추가후 발생 시 순환참조 오류가 발생하지 않고 그대로 동작한다.
결론
2.6.0 버전 이후부터는 순환참조 오류를 미리 잡아주게 되었다. 순환참조 상의 오류때문에 필드, set방식을 안 쓸 이유는 없다.
생성자 의존성 주입 방식의 이점은 런타임 상에서 bean 객체가 변경되지 않는 것을 선호한다는 이유만 남은 것 같다. 하지만 런타임 상에서 bean 객체를 의도적으로 변경하는 사람이 존재할까? 하지만 이런 생각은 이전에 순환참조를 안 만들면 된다는 생각과 동일하기 때문에, 간단하게 막을 수 있다면 막아두는 것이 좋다고 생각한다.
코드의 깔끔함에 있어서 사람들의 견해가 다르다고 생각한다. 누군가는 어노테이션을 사용함에 있어 깔끔하다고 생각하고, 누군가는 어노테이션이 없는 것에 깔끔함을 느낄 수 있다. 만약에 생성자 주입이 직접 설정해야하는 번거로움이 걱정된다면 final이 있는 필드만 생성자를 만들어주는 @RequiredArgsConstructor를 사용하면 될 것 같다.
set방식도 구현하는 방식만 다르지 필드 의존성 주입과의 단점과 동일하여 위에 논한 것으로 충분하다고 생각한다.