Spring plus 트러블 슈팅

SIHA·2025년 3월 21일

Spring Security

AuthUser의 권한 설계 오류 및 해결 과정

문제 상황

Spring Security를 JWT 기반으로 적용하면서 AuthUser 객체를 만들었고, 해당 객체를 JwtAuthenticationToken 생성자에 전달하는 과정에서 오류가 발생했다.

초기 AuthUser 클래스:

@Getter
public class AuthUser {
    private final Long id;
    private final String email;
    private final UserRole userRole;
    private final String nickname;

    public AuthUser(Long id, String email, UserRole userRole, String nickname) {
        this.id = id;
        this.email = email;
        this.userRole = userRole;
        this.nickname = nickname;
    }
}

JwtAuthenticationToken의 생성자는 다음과 같이 Collection<? extends GrantedAuthority>를 요구했으나:

public JwtAuthenticationToken(AuthUser authUser) {
    super(authUser.getAuthorities()); // Collection을 기대
    this.authUser = authUser;
    setAuthenticated(true);
}

여기서 authUser.getUserRole()을 넘기려 했을 때 타입 불일치 문제가 발생했다.

원인

  • super()Collection<? extends GrantedAuthority> 타입을 요구하는데, AuthUser 안에 UserRole이 단일 값으로 정의되어 있었음.
  • UserRole은 enum 타입으로 GrantedAuthority가 아니며, Collection 타입도 아님.
@Getter
public class AuthUser {

    private final Long userId;
    private final String email;
    private final Collection<? extends GrantedAuthority> authorities;

    public AuthUser(Long userId, String email, UserRole role) {
        this.userId = userId;
        this.email = email;
        this.authorities = List.of(new SimpleGrantedAuthority(role.name()));
    }
}

전에 들었던 발제 강의대로 위의 코드처럼 만들고 싶었으나 아래 fromAuthUser()에서 authUser.getUserRole()d을 가져오지 못하는 문제가 생겼다.

public static User fromAuthUser(AuthUser authUser) {
    return new User(authUser.getId(), authUser.getEmail(), authUser.getUserRole(), authUser.getNickname());
}
  • authUser.getUserRole()이 단일 값이 아니라 Collection<UserRole>이 되어 반환 및 매핑이 어려워짐.

시도해 본 방법 및 고민

private final Collection<? extends GrantedAuthority> authorities;

로 필드를 만들고, 그에 맞춰 코드를 수정하고자 했으나 실패하였다.

최종 해결 방법

AuthUser 클래스에 getAuthorities() 메서드를 추가하여 단일 권한을 Collection으로 변환했고, 결국 Collection<UserRole> 필드 대신 단일 UserRole을 유지하며 해결하였다.

아쉬운 점

코드를 수정하는 한이 있더라도 Collection으로 해결했어야 하는데, 그러지 못하고 단일 권한을 Collection으로 변환하는 방식으로 해결한 점이 아쉽다.

최종 AuthUser 코드

@Getter
public class AuthUser {
    private final Long id;
    private final String email;
    private final UserRole userRole;
    private final String nickname;

    public AuthUser(Long id, String email, UserRole userRole, String nickname) {
        this.id = id;
        this.email = email;
        this.userRole = userRole;
        this.nickname = nickname;
    }

    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority(userRole.getUserRole()));
    }
}

Query DSL

Q Class 생성 안됨 문제

    // QueryDSL 추가
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'

위와 같이 QueryDSL을 추가할 수 있도록 dependency를 추가 후 빌드를 했는데, Q클래스가 생성이 안됬다

dependecy 수정

    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'

로 버전을 명시적으로 수정하고 재빌드 하였으나 여전히 문제가 해결되지 않았다.

gradle 설치 후 빌드


Enable annotation processing가 체크가 안되어 있길래 체크했으나 그 이후에도 해결이 안되어서 결국 gradle을 설치하고 ./gradlew clean build 로 해결했다.

@QueryProjection

기존에 가지고 있는 QClass는 모두 Entity 기반의 QClass였는데, ResponseDto(Todo 엔티티의 title, manager수, comment수)의 필드를 반환받기 위해 해당 dto를 QClass로 만들고 싶었다.

@Getter
public class TodoSearchResponse {
    private final String title;
    private final long totalManagers;
    private final long totalComments;

    @QueryProjection
    public TodoSearchResponse(String title, long totalManagers, long totalComments) {
        this.title = title;
        this.totalManagers = totalManagers;
        this.totalComments = totalComments;
    }
}
  • @QueryProjection 을 붙이면 ./gradlew compileQuerydsl 실행 시 QTodoSearchResponse 클래스를 생성할 수 있다.
  • 이후 QueryDSL 쿼리에서 new QTodoSearchResponse(...) 형태로 타입 안전하게 DTO를 반환할 수 있다.

JPAExpressions

QueryDSL 내에서 서브쿼리를 작성할 때 사용하는 기능.

.select(new QTodoSearchResponse(
    todo.title,
    JPAExpressions.select(manager.count())
        .from(manager)
        .where(manager.todo.eq(todo)),
    JPAExpressions.select(comment.count())
        .from(comment)
        .where(comment.todo.eq(todo))
))
  • 특정 엔티티 기준으로 연관 엔티티의 집계 값(예: 매니저 수, 댓글 수)을 가져올 때 유용하다.
  • JPAExpressions 를 통해 서브쿼리를 작성하면 groupBy 없이 각 항목별 count 값을 효율적으로 가져올 수 있다.
  • 주의: where 절에서 .eq(todo) 처럼 엔티티 연결을 정확히 지정해주어야 한다.

BooleanBuilder

  • QueryDSL에서 동적 조건을 if문 조합으로 작성할 때 사용.
  • and(), or() 를 이용해 다양한 조합을 유연하게 적용 가능.
  • 조건이 많고 동적으로 바뀌어야 할 때 유용하지만, if문이 많아지면 코드가 장황해질 수 있다.

예시

BooleanBuilder builder = new BooleanBuilder();
if (title != null) {
    builder.and(todo.title.containsIgnoreCase(title));
}
if (nickname != null) {
    builder.and(todo.managers.any().user.nickname.containsIgnoreCase(nickname));
}

BooleanExpression

  • 메서드 분리로 동적 조건을 깔끔하게 처리하는 방식.
  • null 반환 시 QueryDSL 내부에서 무시되므로 불필요한 조건을 자동으로 걸러준다.
  • 코드의 가독성과 유지보수가 뛰어남.

예시

.where(
    titleContains(title),
    managerContains(nickname),
    createdAtBetween(start, end)
)

private BooleanExpression titleContains(String title) {
    return (title != null && !title.isBlank()) ? todo.title.containsIgnoreCase(title) : null;
}
  • 조건이 단순하거나 함수화해서 깔끔히 관리하고 싶을 때 권장한다.
profile
뭐라도 해보자

0개의 댓글