Persistence와 Persistence Framework

영속성은 프로그램이 종료되도 데이터가 보존되는 것을 말한다. 자바에서는 JDBC(Java Database Connectivity)라 불리는 기술을 통해 메모리에 있는 객체들에게 Persistence를 줄 수 있다(즉 데이터로 저장할 수 있다는 말).

 

 

자바 앱에서 JDBC API를 호출하고, 내부적으로 JDBC driver manager를 이용해 DB와 상호작용하는 것을 도식화한 그림이다. 참고로 MySQL, SingleStore 등 저마다의 DBMS들은 자신들에 맞는 JDBC driver를 제공한다.

 

그러나, 이렇게 JDBC만을 사용하는 것은 매번 JDBC로 DB와 커넥션을 맺고, 쿼리를 날려 수행하고, 결과를 받은 다음 커넥션을 끊는 작업을 프로그래밍해야 한다는 문제가 있었다. 이런 과정을 단순화하여 간단하게 DB와 상호작용하기 위해, 내부적으로 JDBC API를 활용하게끔 하여 개발자의 수고를 덜어주는 Persistence Framework가 등장했으며 대표적으로 SQL Mapper와 ORM이 있다.

 

 

SQL Mapper

개발자가 직접 작성한 SQL 쿼리의 실행 결과를 객체로 바인딩해주는 기술. 대표적으로는 MyBatis가 있다.

SQL에 의존성이 높은 방법으로, MyBatis의 경우 xml파일을 통해 SQL 쿼리를 별도 관리하며 동적 쿼리 작성(실행 중 사용자 입력을 통해 들어오는 파라미터 값에 따라 다른 쿼리가 실행되게 하는 것)을 통해 복잡한 쿼리도 처리할 수 있다는 장점이 있다. DBA같은 밥먹고 DB만 파오신 전문가 분들께 복잡한 쿼리 작성을 짬때리는게 가능해진다(?)는 장점도 있다. 그러나 어찌됐건 개발자가 직접 SQL을 작성해야 한다는 것에서 결국은 공수가 드는 것이며, 데이터 모델 패러다임 불일치(자바는 클래스와 객체를 통해 데이터를 모델링하나 DB는 테이블과 컬럼을 통해 데이터를 모델링한다는 것에서 오는 차이)가 발생한다는 단점이 있다. 즉, 객체지향적인 관점에서의 프로그래밍이 어렵다.

 

 

ORM (Object Relation Mapping)

객체와 DB의 데이터를 자동으로 매핑해주는 기술. 대표적으로는 JPA가 있다.

설정된 관계를 기반으로 자동으로 SQL 쿼리가 생성되며, DBMS에 의존적이지 않아 개발자가 비즈니스 로직에 좀 더 집중할 수 있는 효과를 줄 수 있다. 또한 DB 데이터를 객체로 매핑해주기 때문에 좀 더 객체지향적인 관점에서의 프로그래밍이 용이하다. 자체적으로 복잡한 쿼리의 자동생성은 어렵기 때문에 OLAP성 업무보다는 OLTP성 업무가 많이 쓰일 때 JPA를 활용하면 좋다.

 

참고로 JPA는 자바 진영에서 제공하는 ORM을 위한 표준 기술로 그 자체는 "인터페이스"다. 즉, JPA를 사용하려면 해당 인터페이스를 구현한 구현체를 별도로 사용해야 하며, 대표적으로 Hibernate가 가장 유명하다.

 

 

 

읽기 전에 : 해당 글은 스프링 시큐리티 6.1.2버전 기준으로, 읽는 시점에 따라 다른(deprecate됐다든지) 내용일 수도 있습니다.


어떠한 요청을 보냈을 때, 요청을 보낸 이에 대한 인가/인증이 수반돼야 한다. 이때

 

인증(Authentication) : 자원에 접근하는 사용자의 신원을 검증하는 것(너 누군데?)

인가(Authorization) : 사용자가 그 자원에 대해 접근할 수 있는지 확인하는 것(누군지 알겠는데, 너가 그런 권한이 있어?)

 

그렇다면 스프링 진영에서 보안을 담당하는 대표적인 스프링 하위 프레임워크인 스프링 시큐리티에서는 인증과 인가를 어떤 방식으로 해줄까? 이에 대해 알아보자


그 전에 알고 가면 좋은 지식

SecurityContextHolder

 

https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html#servlet-authentication-securitycontextholder

 

스프링 시큐리티가 인증된 사용자의 정보(Authentication)을 SecurityContext에 저장하는데 이 놈을 가지고 있는 바구니가 바로 SecurityContextHolder다. 한 마디로 여기에 사용자의 정보(Authentication)이 있으면 인증된 것으로 취급하며, 나중에 이렇게 담은 정보(Authentication)을 통해 인가를 한다. 참고로 어떻게 넣는지는 상관없이 걍 여기에 정보(Authentication)가 들어있기만 하면 된다.

 

 

Authentication

Spring Security에서 발급해주는 통행증이라고 생각하면 된다. 이 통행증은 다음과 같은 내용들을 가진다

 

  • Principal : 보안 주체에 해당하는 개념으로, 쉽게 말해 "누구냐"에 해당. 보통 UserDetails라는 "사용자의 정보가 담긴 문서"가 이 역할을 한다.
  • Credential : 주로 비밀번호
  • Authorities : Principal이 뭘 할 수 있는지에 대한 권한, 즉 Principal에게 부여된 권한들이며 GrantedAuthority들이 이 역할을 한다

 


인증

우선 사용자를 인증하는 일반적인 방식은 사용자로부터 하여금 이름과 비밀번호를 입력하도록 요구하는 것이다. 이를 통해 인증이 수행되면 ID(세션 ID)를 발급하고, 이를 통한 인증을 수행할 수 있다. 스프링 시큐리티 역시 이름과 비밀번호를 통한 인증 방식을 지원하며, 다음과 같은 과정을 거쳐 진행된다.

 

https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/basic.html

 

 

  1. BasicAuthenticationFilter(폼로그인의 경우, UsernamePasswordAuthenticationFilter가 이 역할을 함)가 클라이언트로부터 온 HttpServletRequest로부터 username과 password를 뽑아낸다
  2. 뽑아낸 내용을 바탕으로 UsernamePasswordAuthenticationToken(Authentication의 일종)이란 걸 만든다
  3. 사용자를 인증하기 위해 이 토큰을 AuthenticationManager란 놈에게 보낸다
  4. Authenticatoin이 이 토큰을 바탕으로 "인증"을 시도한다
  5. 인증에 실패하면 -> SecurityContextHolder가 비워지고, RememberMeServices의 loginFail이 호출되며, AuthenticationEntryPoint가 호출된다
  6. 인증에 성공하면 -> 해당 인증 정보가 SecurityContextHolder에 설정 즉 저장되고, RememberMeServices의 loginSuccess가 호출되고 filterChain.doFilter를 호출해 남은 로직(다음 필터들에서 진행될 것들)을 수행한다

 

근데 생각해보면.. 매번 요청을 보낼 때마다 이름과 비밀번호를 함께 보내는 방식으로 인증받는 건 당연히 번거롭다. 최초 로그인할 때만 이렇게 하고 SecurityContext를 세션(HttpSession)에 저장한뒤 세션 id를 클라이언트에게 발급한다. 이후 다시 접속하면 세션으로부터 SecurityContext를 가져와 SecurityContextHolder에 꽂아준다. 따라서 해당 SecurityContext가 Authentication을 갖고 있으면 인증상태가 유지된다. (앞서 말했듯, SecurityContext에 Authentication이 들어있어야 인증된 상태라고 취급하기 때문이다)

 

 

여기서 내가 의문이 든 것은, 로그인 이후의 요청에 대해 왜 다시 SecurityContext를 다시 SecurityContextHolder에 꽂아주는가였다. 이미 로그인할 때 Authentication이 들어있는 SecurityContext를 SecurityContextHolder에 넣어줬을텐데? 

하지만 이유는 간단했다. 사용자가 보낸 request를 처리하는 스레드가 종료되면 SecurityContextHolder를 비워주기 때문이다. 그래서 다시 세션으로부터 SecurityContext를 가져와 SecurityContextHolder에 꽂아주는거다.

 

코드레벨에서 보면 쉽게 파악할 수 있다.

 

// 간단히 말하면 필터체인의 앞단에서 SecurityContext를 로드하는 걸 담당하는 필터
public class SecurityContextHolderFilter extends GenericFilterBean {
	// ..생략
	private final SecurityContextRepository securityContextRepository;
	// ..생략

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws ServletException, IOException {
		// 대충 처음 접속하면 이 if조건문의 doFilter가 실행됨
		if (request.getAttribute(FILTER_APPLIED) != null) {
			chain.doFilter(request, response);
			return;
		}
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
		// SecurityContext를 가져오는 모습
		Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
		try {
			this.securityContextHolderStrategy.setDeferredContext(deferredContext);
			chain.doFilter(request, response);
		}
		finally {
			// 마지막에 SecurityContextFilter를 비워내는 모습!
			this.securityContextHolderStrategy.clearContext();
			request.removeAttribute(FILTER_APPLIED);
		}
	}
    
	// ..생략

 

다음과 같이 Session에서 SecurityContext를 가져오는 모습도 코드레벨에서 훔쳐볼 수 있다

 

public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
	// ..생략

	@Override
	public DeferredSecurityContext loadDeferredContext(HttpServletRequest request) {
		// 세션으로부터 SecurityContext를 가져오는 모습
		Supplier<SecurityContext> supplier = () -> readSecurityContextFromSession(request.getSession(false));
		return new SupplierDeferredSecurityContext(supplier, this.securityContextHolderStrategy);
	}
    
	// ..생략

 

 

참고로, SecurityContextHolder는 ThreadLocal을 사용한다. 즉 SecurityContextHolder는 여러 쓰레드가 공유하는 요소가 아니다.

 

// SecurityContextHolder의 ThreadLocalSecurityContextHolderStrategy 코드 일부
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

	private static final ThreadLocal<Supplier<SecurityContext>> contextHolder = new ThreadLocal<>();

	@Override
	public void clearContext() {
		contextHolder.remove();
	}

	@Override
	public SecurityContext getContext() {
		return getDeferredContext().get();
	}
    
    // ... 생략

 

보다시피 ThreadLocal을 내부적으로 사용하는 모습을 볼 수 있다. 각 쓰레드별로 서로 다른 내용물을 갖는 SecurityContextHolder를 쓰게 되는 것이며, 이를 통해 사용자별로 SecurityContextHolder내에 서로 다른 인증객체(Authentication)을 가지게 되는 효과를 준다! 동일한 쓰레드 내에선 언제든지 SecurityContext를 참조가능하다.

 

ThreadLocal : 쓰레드마다 할당되는 자신만의 고유한 공간. 멀티쓰레드 환경이어도 ThreadLocal은 다른 쓰레드로부터 안전한 공간이다.

 

 

즉 정리하자면

 

  1. 사용자별로 보내는 요청들을 쓰레드를 통해 처리한다
  2. username, password기반으로 이런 요청들에 대한 인증 작업을 한다. 인증이 완료되면 SecurityContext에 Authentication을 꽂아넣으며, Spring Security는 SecurityContext에 Authentication이 들어가있는지 여부를 통해 인증됐는지 아닌지를 판단한다.
  3. 근데 매번 username, password를 함께 보낼 순 없으니 세션을 활용한다. 세션id를 통해 세션으로부터 SecurityContext를 가져와 이를 SecurityContextHolder에 꽂아넣는 식이다.
  4. 사용자별 Authentication 구분은 SecurityContextHolder가 ThreadLocal을 사용함으로써 가능하다.

 


인가

인증 과정이 끝났다면, SecurityContextHolder에 SecurityContext가 들어있고 이 안에 Authentication이란 "통행증"이 들어있으며 이를 통해 인가를 진행할 수 있다. Authentication은 내부적으로 Principal(보안 주체에 해당하는 개념)의 authorities(권한)들을 가지며, 구체적으론 GrantedAuthority인터페이스의 구현체들이 Collection형태로 들어가 있다. 

 

GrantedAuthority의 getAuthority()메서드를 통해 권한 정보들을 읽을 수 있으며, AccessDicisionManager라는 놈이 이 권한 정보들을 읽어서 접근 가능한지 아닌지를 판단해준다.

 

인가는 다음 과정을 통해 진행된다.

 

https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html

 

  1. 필터체인의 AuthorizationFilter가 SecurityContextFilter로부터 Authentication(통행증)을 받아온다. 
  2. 만약 Authentication을 받아오는데 실패하면 AuthenticationException을 던진다.
  3. AuthorizationManager(얘가 AccessDicisionManager역할도 해준다)에게 받아온 Authentication과 HttpServletRequest를 전달한다
  4. AuthorizationManager가 해당 request를 등록된 패턴과 match되는지를 판단해준다
  5. 거부됐다면 -> AuthorizationDeniedEvent가 발행되고 AccessDeniedException을 던진다
  6. 승인됐다면 -> AuthorizationGrantedEvent가 발행되고 필터체인을 마저 진행한다

 

AuthorizationFilter에서 Authentication을 받아오는 메서드의 코드는 다음과 같으며, Authentication을 못 받아오면 예외를 던지는 모습을 직관적으로 바로 확인가능하다.

 

private Authentication getAuthentication() {
	Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
	if (authentication == null) {
		throw new AuthenticationCredentialsNotFoundException(
			"An Authentication object was not found in the SecurityContext");
	}
	return authentication;
}

 

참고로 AuthenticationCredentialsNotFoundException은 AuthenticationException을 상속받은 클래스다.

 

또한 디폴트 설정은 필터체인의 마지막 필터가 AuthorizationFilter이니 알아두면 좋다.

 


인증/인가 실패 시에 대한 예외처리

인증에 실패하면 AuthenticationException이, 인가에 실패하면 AccessDeniedException이 던져진다. 이들은 어떻게 처리되는가?

바로 필터체인의 ExceptionTranslationFilter에서 처리된다.

 

 

이 필터에서 doFilter를 통해 필터체인의 로직들을 수행하다가, AuthenticationException이나 AccessDeniedException이 발생하면 그에 대한 처리를 해주는거다.

 

유저가 인증되지 않았거나 AuthenticationException이 발생하면

 

  1. SecurityContextHolder를 비움
  2. 인증 성공 후 다시 replay가 가능하게끔 클라이언트에게 온 request를 저장
  3. AuthenticationEntryPoint라는 놈을 이용해 클라이언트에게 자격 증명을 요청(즉 클라이언트에게 너 누군지 신원을 대라고 하는 거다. 여기선 AuthenticationEntryPoint가 인증 실패시 써먹는 놈이라고만 알고 넘어가면 된다)

 

AccessDeniedException이 발생하면, 심플하다. 등록된 AccessDeniedHandler를 호출한다.

 

이를 수도 코드로 보면 다음과 같다고 보면 된다.

 

try {
	filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
	if (!authenticated || ex instanceof AuthenticationException) {
		// 클라이언트에게 자격 증명을 요청(인증 실패했으니까)
		startAuthentication();
	} else {
		// AccessDeniedHandler 호출
		accessDenied();
	}
}

 

실제로 ExceptionTranslationFilter 내부를 뜯어보면 다음과 같은 코드를 볼 수 있다.

 

private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
	FilterChain chain, RuntimeException exception) throws IOException, ServletException {
	if (exception instanceof AuthenticationException) {
		handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
	}
	else if (exception instanceof AccessDeniedException) {
		handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
	}
}

 

handleAuthenticationException메서드는 내부적으로 sendStartAuthentication메서드를 호출하며, 코드는 다음과 같다.

 

protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
	AuthenticationException reason) throws ServletException, IOException {
	// SEC-112: Clear the SecurityContextHolder's Authentication, as the
	// existing Authentication is no longer considered valid
	SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
	this.securityContextHolderStrategy.setContext(context);
	this.requestCache.saveRequest(request, response);
	this.authenticationEntryPoint.commence(request, response, reason);
}

 

request를 저장하고, AuthenticationEntryPoint의 commence라는 메서드를 호출하는 모습을 볼 수 있다.

handleAccessDiniedException메서드는 AccessDiniedHandler의 handle메서드를 호출한다. 

 

나중에 본인이 커스텀한 AuthenticationEntryPoint와 AccessDeniedHandler를 이용해, 인증/인가를 실패했을 때에 대한 처리방식을 직접 지정해줄 수도 있어서 이렇게 설명해봤다.

 

 


스프링 시큐리티 원리에 대한 내용을 계속 구글링하면서 뒤져봤는데, 구글링해서 보는 페이지마다 다 같은 내용들만 써져 있어서 내가 궁금한 내용들에 대한 서치가 힘들었다. 각 용어들에 대한 개념은 블로그들을 보면서 어느 정도 알겠는데, 어떤 과정으로 진행되는지에 대한 궁금증들이 있었다. 그러던 중 그냥 공식 문서를 파보자는 생각으로 공식 문서들을 파봤다.

 

사실 이전에 React 공부할 때를 빼면, 공식문서를 이렇게까지(물론 지금도 엄청나게 한 것도 아니지만) 공부한 적은 없었다. 개발자로 산다는 사람이 공식문서를 진득하게 본 적 없다는게 사실 생각해보면 부끄러운 일인 것 같다. 처음부터 공식문서를 보는건 개념 잡기가 어렵다는 생각이 들어 지금껏 새로운 걸 공부할 때마다 공식문서로 시작하는 걸 기피해왔던 것 같기도.. 근데 이번에 느낀게, 구글링해서 블로그들 보면서 어느 정도 용어들에 대한 개념만 잡고 그 후엔 공식문서로 공부하는게 되게 좋은 방법 같다. 오피셜한 문서인 만큼 내용에 대한 의심이 안 들고, 정리가 잘 돼있다. 지금 다룬 스프링 시큐리티만 해도 글을 작성하는 현재를 기준으로 구글링해서 나오는 블로그들은 다 옛날 버전이라 deprecate된 내용들이 많다. 예를 들면 인가 과정에서 FilterSecurityInterceptor를 쓴다고들 하는데 6.1.2버전 기준 FilterSecurityInterceptor는 deprecate됐다. 또 SecurityContextPersistenceFilter도 deprecate됐다. 이들 대신에 AuthorizationFilter와 SecurityContextHolderFilter를 현재는 사용중이다.

 

암튼 앞으로 내가 모르는 기술들을 공부할 땐, 구글링해서 개념만 대강 잡고 공식문서로 시작하는게 꽤나 좋은 방법인 듯. 나만의 공부방법을 정립해가야할 때다.

 

 

 

참고

https://catsbi.oopy.io/f9b0d83c-4775-47da-9c81-2261851fe0d0

 

스프링 시큐리티 주요 아키텍처 이해

목차

catsbi.oopy.io

https://docs.spring.io/spring-security/reference/index.html

 

Spring Security :: Spring Security

If you are ready to start securing an application see the Getting Started sections for servlet and reactive. These sections will walk you through creating your first Spring Security applications. If you want to understand how Spring Security works, you can

docs.spring.io

 

프로젝트를 진행하면서, 개의 중성화여부를 나타내는 컬럼을 추가해달라는 요청을 받았다.

true / false값을 저장하는 is_neutered필드를 추가하기로 했고, 찾아보니 MySQL에서는 boolean타입을 별도로 지원하지 않는다고 해서 TINYINT(1)타입으로 필드를 만들어줬다. 1은 true, 0은 false를 나타낼 수 있게끔.

 

 

// Dog entity
    @Column(columnDefinition = "TINYINT(1)", nullable = false)
    private boolean isNeutered;

 

그리고 강아지를 등록할 때 사용하는 dto들에도 마찬가지로 isNeutuerd라는 필드들을 추가해줬다.

 

@Getter
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class SignupRequestDto {
    private String email;
    private String name;
    private String dogName;
    private String dogType;
    private String dogGender;
    private boolean isNeutered;
    private int dogAge;
    
    // ... 생략

 

근데 이윽고 잠시 후, 문제가 생겼다. 실제 DB에 쌓이는 데이터들을 보는데, dog테이블의 is_neutered컬럼이 죄다 0만 저장되고 있던 거다.

 

참고로 프론트(플러터 단)에서는 요로코롬 true값을 담아서 request를 보내고 있던 상황이었음

 

      var res = await dio.post(
        "http://localhost:8080/api/auth/signup?provider=$oauth2Provider",
        data: {
          "email": email,
          "name": _userName,
          "dog_name": _dogName,
          "dog_type": _dogType,
          "dog_gender": _dogGender,
          "is_neutered": true,
          "dog_age": _dogAge,
        },
      );

 

뭘까 싶어서 컨트롤러에서 SignupRequestDto의 값을 찍어봤는데, 이럴 수가!! (ㅎㅎ..내가 봐도 오글거리는 말투다)

isNeutered필드 값이 false가 찍히고 있었다!

 

단순히 이 현상만 보자면 프론트에서는 true를 넘겼는데 백엔드에서 받고 보니까 false다..? 중간에 해커가 탈취해서 조작했나..? 오만가지 생각이 다 들었다.

 

하지만 구글링을 통해 비교적 쉽게 문제를 해결할 수 있었다. 바로 dto에서 boolean이 아니라 Boolean으로 타입을 변경해주는 것을 통해서!

 

 

일단 원인부터 정리하자면 다음과 같았다

 

  • 롬복(Lombok)은 @Getter나 @Setter를 붙이면 필드들에 대해 get뭐시기, set뭐시기같은 게터 & 세터 메소드들을 만들어준다.
  • 근데 만약 필드가 boolean타입이면서 is로 시작하는 필라면(ex: isExpired), getIsExpired나 setIsExpired같은 이름으로 메서드를 만들어주는게 아니라 isExpired, setExpired란 이름의 게터와 세터를 만들어준다

 

결국 내가 SignupRequestDto에 작성한 isNeutered라는 필드에 대한 게터가 getIsNeutered가 아닌 isNeutered로 작성된 것. 근데 이게 왜 문제가 되는가? 바로 컨트롤러에 있는 @RequestBody에서 문제가 된다.

 

    @PostMapping("/signup")
    public BaseResponse<JwtResponseDto> signup(@RequestParam("provider") String oauth2ProviderName,
                                               @RequestBody SignupRequestDto signupRequestDto) {
        Oauth2Provider oauth2Provider =
                Oauth2Provider.getOauth2ProviderByName(oauth2ProviderName);
        
        // ... 생략

 

@RequestBody가 어떻게 동작하길래 dto의 게터가 isNeutered인게 문제가 되는 걸까?

 

알다시피 이 놈은 client가 보낸 request의 json데이터를 Dto에 매핑해주는 역할(역직렬화)을 한다.

MappingJackson2HttpMessageConverter가 내부적으로 ObjectMapper를 활용해서 값을 dto에 바인딩해주는데, 이 때 dto에 게터들이 있어야 한다. 즉 get뭐시기라는 형식의 이름을 가진 메서드들이 있어야 하는데, 롬복선생께서 isNeutered의 게터를 getIsNeutered가 아니라 isNeutered로 만들어줘서 문제가 생기는 것. (적절한 게터를 찾게 되지 못하는 것이기 때문) 이때 boolean타입의 기본값은 false기 때문에 자동으로 false가 들어가져있던 거다.

 

즉 이를 해결하기 위해선 이리 볶든 저리 볶든 Dto클래스에 getIsNeutered라는 게터가 있으면 되는거다.

물론 내가 직접 게터를 dto클래스에 만들어주면 되나! 필드의 타입을 boolean이 아닌 Boolean으로 작성(즉 래퍼클래스를 쓰는 형태로 작성)하면, is라는 접두사로 시작하는 필드여도 get뭐시기 라는 형태로 만들어진다고 한다.

 

@Getter
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class SignupRequestDto {
    private String email;
    private String name;
    private String dogName;
    private String dogType;
    private String dogGender;
    private Boolean isNeutered;
    private int dogAge;
    
    // ... 생략

 

 


✅ 참고한 글들

 

https://jungguji.github.io/2020/12/31/RequestBody-Annotation-%EC%82%AC%EC%9A%A9-%EC%8B%9C-boolean-%EB%B3%80%EC%88%98-%EB%B0%94%EC%9D%B8%EB%94%A9-%EC%97%90%EB%9F%AC/

 

RequestBody Annotation 사용 시 boolean 변수 바인딩 에러

서론토이프로젝트 중 @Reqeust 어노테이션을 적용한 DTO에서 boolean 데이터를 제대로 전달 받지 못하는 문제가 발생하여 이를 정리한다. 문제 발생vue.js에서 넘어온 데이터를 @RequestBody 어노테이션을

jungguji.github.io

https://sedangdang.tistory.com/305

 

@RequestBody 공식문서를 읽어보자 + HttpMessageConverter

의역/오역이 있을 수 있고 잘못된 부분이 존재할 수 있습니다 요약 1. @RequestBody는 HttpMessageConverter가 HTTP Request Body 내의 데이터를 객체로 변환(역직렬화)하도록 시키는 애노테이션이다. 2. @Valid를

sedangdang.tistory.com

https://velog.io/@soyeon207/Spring-Lombok-%EC%82%AC%EC%9A%A9%EC%8B%9C-is-prefix-%EB%8A%94-%EC%99%9C-%EC%97%86%EC%96%B4%EC%A7%88%EA%B9%8C

 

Lombok 사용시 is prefix 는 왜 없어질까 ?

isNew 라는 신상품 여부를 나타내는 변수가 있었는데 response 로 DTO 내려줄 때 is 가 사라지고 new 라고만 계속 보여졌다.

velog.io

 

@Value를 사용해 application.properties에 적은 값을 가져오는 과정, 구체적으론 jwt secret key를 가져오는 과정에서 null값을 가져오는 문제가 생겨버렸다.

 

이상하게도 AccountController에서 jwt관련 기능들을 쓸 때는 null이 아니라 제대로 된 값을 가져오는데, 필터 쪽에서 jwt 토큰 인증을 할 때에만 null로 가져오는 게 문제였다..

 

원인은 찾아보니 알 수 있었다. 바로 AccountController에서는 jwt key를 담는 멤버변수가 있는 객체가 빈으로 등록된 애였지만, 같은 jwt key를 담는 멤버변수를 가지는 필터는 빈으로 등록된 애가 아니었던 것

 

public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.csrf().disable() // CSRF 공격에 대한 방어를 해제
                .cors().and() // cors 허용
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 서버를 stateless하게 유지 즉 세션 X
                .and()
                .formLogin().disable() // 시큐리티가 기본 제공하는 로그인 화면 없앰. JWT는 로그인과정을 수동으로 클래스로 만들어야 하니까
                .httpBasic().disable() // 토큰 방식을 이용할 것이므로 보안에 취약한 HttpBasic은 꺼두기
                .authorizeRequests() // HttpServletRequest를 사용하는 요청들에 대한 접근제한을 설정하겠다
                .requestMatchers("/account/signup", "/account/login", "/").permitAll() // 요 세 놈에 대한 요청은 인증없이 접근 허용
                .anyRequest().authenticated() // 나머지에 대해선 인증을 받아야 한다.
                .and()
                // 여러 필터들 중 UsernamePassword필터 앞에 내가 만든 필터를 둔다. 이렇게 하면 커스텀 필터로 인가인증을 다룰 수 있음
                .addFilterBefore(new JwtAuthenticationFilter(tokenProvider),
                        UsernamePasswordAuthenticationFilter.class)
                .build();
    }

 

보이다시피 new 연산자를 통해 새로운 필터객체를 만들어서 끼워주는 모습을 볼 수 있다. 이 필터객체가 @Value 어노테이션을 통해 jwt key를 가져와야 하던 놈이다..근데 빈이 아니라 못 가져오던 것! 뭐, 해결법은 간단하다. 필터를 빈으로 만들어 끼우면 된다! 

 

private final JwtAuthenticationFilter jwtAuthenticationFilter;

public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.csrf().disable() // CSRF 공격에 대한 방어를 해제
                .cors().and() // cors 허용
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 서버를 stateless하게 유지 즉 세션 X
                .and()
                .formLogin().disable() // 시큐리티가 기본 제공하는 로그인 화면 없앰. JWT는 로그인과정을 수동으로 클래스로 만들어야 하니까
                .httpBasic().disable() // 토큰 방식을 이용할 것이므로 보안에 취약한 HttpBasic은 꺼두기
                .authorizeRequests() // HttpServletRequest를 사용하는 요청들에 대한 접근제한을 설정하겠다
                .requestMatchers("/account/signup", "/account/login", "/").permitAll() // 요 세 놈에 대한 요청은 인증없이 접근 허용
                .anyRequest().authenticated() // 나머지에 대해선 인증을 받아야 한다.
                .and()
                // 여러 필터들 중 UsernamePassword필터 앞에 내가 만든 필터를 둔다. 이렇게 하면 커스텀 필터로 인가인증을 다룰 수 있음
                .addFilterBefore(jwtAuthenticationFilter,
                        UsernamePasswordAuthenticationFilter.class)
                .build();
    }

 

 

결론!

@Value어노테이션을 통해 값이 멕여지는 멤버변수를 가지는 객체는 빈으로 등록돼있어야 값을 잘 가져온다.


근데 왜 Bean으로 등록해야 가져와지는 거지?

결론적으로 Bean 생성과정에서 @Value가 inject되기 때문..

구체적인 과정들은 현재 내 수준에선 뭔 말인지 잘 모르기 때문에 생략(ㅠ)

 


 

참고한 링크

https://wildeveloperetrain.tistory.com/143

 

@Value 어노테이션 null이 나오는 문제 해결 방법

@Value annotation을 사용하여 properties에 있는 메타 정보 값을 가져오는 과정에서, 값이 null으로 들어오는 문제를 해결하며 기록한 내용입니다. @Value Annotation 쉽게 @Value 어노테이션은 데이터베이스

wildeveloperetrain.tistory.com

https://duooo-story.tistory.com/32

 

@Value는 어디서 set이 되는걸까?

프로젝트에서 reousrce로 등록한 데이터를 @value를 통해서 데이터를 받아 사용하고 있습니다. 이 기능 덕분에 각 존별로 다른데이터를 코드의 분기처리없이 사용하고 있습니다. @Configuration public cla

duooo-story.tistory.com

 

스터디를 진행하던 중 멘토님이 Lombok라이브러리를 사용해서 스터디를 진행하셨다. 하지만 스프링에 막 발을 들인 나는 롬복이 뭔지 모른다. 그래서 정리해봤다.


롬복?

어노테이션 기반으로 코드를 자동완성해주는 라이브러리라고 한다. 개발자 편의를 위해 쓰는 라이브러리인 듯 하다. 다른 언어도 마찬가지지만 자바 언어 역시 기계적으로 작성해야 하는 코드들이 상당히 많이 생기는데 그런 부분들을 자동화해주는 라이브러뤼. 사용한다면 귀찮은 부분들을 작성해주는 걸 편하게 할 수 있을 뿐더러 코드의 길이 자체가 줄어드는 효과를 얻는다는 이점이 있다.

 

 

활용예시 - Getter, Setter

클래스를 만들었다. 이 놈이 갖는 필드 하나하나에 대해 getter, setter를 하나하나 만들어줘야 하는데 여간 귀찮은 게 아니다. 이럴 때 롬복을 이용해 Getter, Setter어노테이션을 주면 지가 알아서 필드들에 대한 getter, setter들을 만들어준다.

 

@Getter
@Setter
public class Member {
    private Long id;
    private String name;
}

 

참고로 클래스 이름 위에 이 어노테이션을 작성하면 모든 필드들에 대해 적용되고, 필드 이름 위에 작성하면 해당 필드에만 적용된다.

 

 

활용예시 - NoArgsConstructor

빈 기본 생성자를 만들어준다.

 

@NoArgsConstructor
public class Member {
    private Long id;
    private String name;
    
   /* 이걸 자동으로 만들어준다
   public Member() {
   }
   */
}

 

 

활용예시 - AllArgsConstructor

모든 필드에 대한 생성자를 만들어준다. (DI(Dependency Injection)를 해주는 식으로)

 

@AllArgsConstructor
public class Member {
    private Long id;
    private String name;
    
   /* 이걸 자동으로 만들어준다
   public Member(Long id, String name) {
        this.id = id;
        this.name = name;
   }
   */
}

 

 

활용예시 - RequiredArgsConstructor

특정 필드들에 대한 생성자를 만들어준다. 도대체 어떤 필드들에 대해 해주냐? final이 붙은 필드들, 그리고 @NonNull 어노테이션이 붙은 필드들에 대해서 자동으로 생성자를 만들어준다. AllArgsConstructor와 마찬가지로 DI(Dependency Injection)을 해주는 생성자를 만들어준다. 

 

※ NonNull 어노테이션 : 롬복에서 쓰는 어노테이션으로, 얘가 멕여진 필드가 null이 되면 NullPointerException을 일으킨다.

 

@RequiredArgsConstructor
public class Member {
    private final Long id;
    @NonNull
    private String name;
    private int age;
    
   /* 이걸 자동으로 만들어준다
   public Member(Long id, String name) {
        this.id = id;
        this.name = name;
   }
   */
}

 

 

이 외에도 어노테이션들이 많다. 클래스에 대한 equals함수와 hashCode함수를 자동으로 만들어주는 @EqualsAndHashCode, 필드들을 기반으로 ToString메서드를 자동으로 만들어주는 @ToString, 객체 생성에 Builder패턴을 적용해주는 @Builder.. 암튼 다양하다. 그때그때 찾아가며 공부하면 될 듯.


스프링에서 @RequiredArgsConstructor를 사용한 생성자 주입

기존에 내가 알고 있던(물론 스프링 공부 시작한지 별로 안됨) 생성자 주입 방식은 이거였다.

 

@Service
public class MemberService {
    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

 

(참고로 memberRepository같은 빈들은 스프링 컨테이너가 관리해준다고 하는데 얘네가 싱글톤 객체라..변하지 않기 때문에 final키워드를 붙이는 게 좋다고 한다)

 

여기서 잠깐 살펴볼 것이, 스프링은 생성자가 1개뿐이라면 @Autowired를 생략해도 된다. 이 경우 @Autowired를 자동으로 인식해 처리하기 때문. 즉 위 코드에서 @Autowired를 빼도 문제가 없다. 

 

그리고 @RequiredArgsConstructor는 final이 붙은 필드들에 대한 생성자를 알아서 만들어준다! 따라서 위 코드를 다음과 같이 아주 편하게 간소화시킬 수 있다.

 

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
}

 

원리는 간단하다.

 

  • RequiredArgsConstructor가 생성자를 만들어줌
  • 그 생성자에 대한 Autowired를 자동으로 인식해 처리함

+ Recent posts