[TIL]2025.05.01

기 원·2025년 5월 1일

[Project] Spring-plus

목록 보기
2/8

QueryDSL 리 마인드

1. QueryDSL 장점

  1. 타입 안전성

    • 컴파일 타임에 오류 잡아줌. 문자열로 쿼리 짜는 JPQL보다 안전함.

    • 오타, 컬럼 이름 틀림 -> 컴파일 에러로 바로 확인 가능.

  2. IDE 자동완성

    • Q클래스를 기반으로 작성하니까 IDE가 필드 자동완성해줌.

    • 실수 줄고 개발 속도 상승

  3. 가독성과 유지보수성

    • 복잡한 동적 쿼리도 자바 코드처럼 명확하게 표현 가능.

    • 조건문 분기 같은 것도 자연스럽게 if문으로 처리 가능.

  4. 동적 쿼리 작성 쉬움

    • BooleanBuilder나 where 조건에 null 무시 기능으로 조건 선택적으로 붙이기 간단.
  5. 쿼리 재활용 쉬움

    • 공통 조건들을 메서드로 빼서 재사용 가능.

2. QueryDSL 단점

  1. 기본 세팅이 귀찮음

  2. 러닝 커브 존재

  3. 코드가 길어짐

3. 언제 QueryDSL를 쓰는게 좋을까?

  1. 복잡한 검색/필터 조건이 많은 경우

    • 회원을 이름, 나이, 주소, 등록일 등으로 조합해서 검색해야 할 경우

    • if != null 조건이 여러 개 붙는 경우
      -> JPQL 이나 Criteria으로 하려면 머리 터짐

  2. 동적 쿼리가 자주 필요한 경우

    • 검색 조건이 optional인 경우 (검색창 입력 여부에 따라 필터 적용)

    • BooleanBuilder 또는 where(x, y, z) 구조로 처리 가능

  3. 리포지토리 쿼리 재사용하고 싶은 경우

    • 쿼리 조건들을 메서드로 분리해서 공통화 가능
      -> 유지보수, 테스트 쉬워짐
  4. 프론트에서 정렬, 페이징 요청 자주 들어올 때

    • orderBy, offset, limit 등 DSL로 처리 가능

    • Pageable과 연동도 쉬움

  5. 엔티티 간 join이 많은 경우

    • fetch join, left join 등 복잡한 join이 필요할 때 더 안전하게 처리 가능

4. 간단한 비교 예시

// JPQL
String jpql = "SELECT m FROM Member m WHERE m.age > :age";

// QueryDSL
QMember m = QMember.member;
List<Member> result = queryFactory
    .selectFrom(m)
    .where(m.age.gt(20))
    .fetch();

5. 결론!

  • 복잡한 검색 조건이 많고, 동적 쿼리 많이 짜야 하면 무조건 쓰는 게 좋다.

개별 프로젝트

Lv. 2

1. Spring Security

Spring 기반 애플리케이션의 인증(Authentication)과 인가(Authorization)를 처리해주는 보안 프레임워크

핵심 기능

  1. 인증(Authentication)
    사용자가 누구인지 확인 (로그인 처리)
    ex) 아이디/비번으로 로그인, 소셜 로그인, JWT 토큰 검증 등
  2. 인가(Authorization)
    인증된 사용자가 어떤 리소스에 접근 가능한지 검사
    ex) 관리자만 /admin, 점주만 /store/** 접근 가능
  3. 보안 관련 필터 자동 적용
    CSRF 방어, 세션 고정 방지, 비밀번호 암호화 등

기존 FilterArgument Resolver를 사용하던 코드들을 Spring Security로 변경

0. build.gradle에 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-security'

1. JwtUtil 클래스에 resolveToken() 및 validateToken() 메서드 추가

    public String resolveToken(HttpServletRequest request) {
        String bearer = request.getHeader("Authorization"); // 토큰값 헤더에서 가져오기
        if (StringUtils.hasText(bearer) && bearer.startsWith("bearer ")) { // 값이 있고, bearer 로 시작한다면
            return bearer.substring(7); // 앞의 "bearer "(7자리)를 때고 토큰값만 리턴 할것
        }
        return null;
    }

    public boolean validateToken(String token) {
        try {
            extractClaims(token); //토큰에서 클레임을 추출하여 유효성 검사
            return true; // 예외 없으면 유효한 토큰으로 판단
        } catch (Exception e) { 
            return false; // 예외 발생 시 유효하지 않은 토큰으로 판단
        }
    }

2. JwtAuthenticationFilter 클래스 설계 -> 기존의 Filter 대체

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

	private final JwtUtil jwtUtil;

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws
		ServletException, IOException {

		String token = jwtUtil.resolveToken(request); // 요청 헤더에서 토큰값 추출

		if (token != null & jwtUtil.validateToken(token)) { // 토큰이 존재하고, 유효성 검증메서드를 통과 할 경우
			Claims claims = jwtUtil.extractClaims(token); //토큰에서 클레임(정보) 추출

			Long userId = Long.parseLong(claims.getSubject()); // 사용자 ID를 클레임에서 꺼냄
			String email = claims.get("email", String.class); // 사용자 email를 클레임에서 꺼냄
			String role = claims.get("userRole", String.class); // 사용자 role을 클레임에서 꺼냄
			String nickname = claims.get("nickname", String.class); //사용자 nickname을 클레임에서 꺼냄

			AuthUser authUser = new AuthUser(userId, email, UserRole.of(role), nickname);
			List<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("ROLE_" + role)); // Spring Security에서 사용할 권한 정보 생성

			UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(authUser, null, authorities); // 인증 객체 생성 / 비밀번호는 null

			SecurityContextHolder.getContext().setAuthentication(authenticationToken); // SecurityContext에 인증 객체 저장
			System.out.println(role);
			System.out.println(authorities);
			System.out.println(authenticationToken);
		}
		filterChain.doFilter(request, response); // 다음 필터로 요청 전달
	}
}

3. SecurityConfig 설계

	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http
			.csrf(AbstractHttpConfigurer::disable) // CSRF(사이트 간 위조 요청) 보안 비활성화 - JWT 기반에서는 필요 없음
			.sessionManagement(session -> session // 세션을 사용하지 않음 - JWT 사용
				.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
			.authorizeHttpRequests(auth -> auth // 요청별 인가 규칙
				.requestMatchers("/auth/**").permitAll() // /auth/** 경로는 누구나 접근 가능
				.requestMatchers("/admin/**").hasRole("ADMIN") // /admin/** 경로는 어드민 만 접근 가능
				.anyRequest().authenticated() // 그 외 모든 요청은 인증 필요
			)
			.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // JWT 인증 필터 등록 (기본 인증 필터 앞에 위치)
			.formLogin(AbstractHttpConfigurer::disable) // 기본 로그인 폼 사용 안함 - JWT 사용
			.httpBasic(AbstractHttpConfigurer::disable); // HTTP Basic 인증 사용 안 함 (ID/PW를 매 요청마다 보내는 방식)

		//최종 보안 설정을 적용한 SecurityFilterChain 반환
		return http.build();
	}

4. PasswordEncoderConfig 설개

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordEncoderConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

여기서 문제
기존의 PasswordEncoder 커스텀 클래스의 처리

  • 이름이 PasswordEncoder라서 Spring Security의 인터페이스랑 이름이 겹쳐서 충돌 남
  • Spring Security 기반으로 전환 중 임으로, 커스텀 로직은 불필요
  • BCryptPasswordEncoder는 Spring Security에서 안정적이고 표준적으로 제공
    -> 결론 지워 버리자

수정 했으니 해당 클레스를 사용하는 서비스의 import 수정 필요
1. auth.AuthService
2. user.UserService

//삭제
import org.example.expert.config.PasswordEncoder;
//추가
import org.springframework.security.crypto.password.PasswordEncoder;

5. 필요 없어진 java파일 정리

  1. JwtFilter.java
    • 기존에 Filter 인터페이스 상속해서 토큰 처리하던 클래스
      -> Spring Security의 JwtAuthenticationFilter로 대체됐으므로 삭제
  2. FilterConfig.java
    • JwtFilter를 등록하던 FilterRegistrationBean 설정 클래스
      -> SecurityFilterChain에서 관리하므로 필요 없음
  3. AuthUserArgumentResolver.java
    • @Auth 어노테이션 + 커스텀 객체 AuthUser 주입 기능 제공
      -> @AuthenticationPrincipal로 대체 가능
  4. WebConfig.java
    • AuthUserArgumentResolver를 등록하던 설정 클래스
      -> 위 클래스 제거하면 이 설정도 쓸모 없음
  5. PasswordEncoder.java
    • BCrypt 직접 썼던 커스텀 인코더
      -> PasswordEncoderConfig로 대체됨, 이름 충돌 및 중복 문제 있음

도전과제 최종 테스트와 문제

1. Not-null property references a transient value - transient instance must be saved before current operation -> Todo.user -> User

원인 : 컨트롤러 단에서 @Auth 사용함 -> JWT토큰으로 변경하며 AuthUserArgumentResolver 삭제
null 발생

해결: @Auth 에서 @AuthenticationPrincipal으로 변경

profile
노력하고 있다니까요?

0개의 댓글