TIL/[TIL]

[TIL]Spring Security에서 CQRS 적용과 사용자 조회 흐름

namerong 2026. 6. 24. 11:36

1. 학습 주제

인증 기능 위에 사용자 조회 기능을 추가하면서, 명령과 조회를 분리하는 CQRS 방식을 조금 더 구체적으로 다뤘다.
회원가입, 로그인처럼 데이터를 변경하는 작업과 사용자 정보 조회처럼 데이터를 읽는 작업을 나눠서 구성하고, 조회 쪽은 MyBatis를 사용해 처리하는 흐름을 정리했다.

  • Command와 Query를 분리하는 이유
  • 사용자 본인 정보 조회 API
  • 관리자 전용 사용자 목록 조회 API
  • @AuthenticationPrincipal로 로그인 사용자 정보 꺼내기
  • @PreAuthorize를 이용한 메서드 단위 권한 제어
  • JPA와 MyBatis를 함께 사용하는 구조 이해

2. CQRS란 무엇인가

CQRS는 Command Query Responsibility Segregation의 줄임말이다.
말 그대로 데이터를 변경하는 책임과 조회하는 책임을 분리하는 방식이다.

Command

  • 데이터를 생성, 수정, 삭제하는 작업
  • 예: 회원가입, 비밀번호 변경, 사용자 삭제

Query

  • 데이터를 읽는 작업
  • 예: 내 정보 조회, 사용자 목록 조회

이 둘을 나누는 이유는 역할이 다르기 때문이다.

  • 변경 작업은 정합성과 트랜잭션이 중요하다.
  • 조회 작업은 빠르고 필요한 형태로 데이터를 가져오는 것이 중요하다.

그래서 보통

  • Command는 JPA
  • Query는 MyBatis나 JPQL, QueryDSL

처럼 다르게 가져가는 경우가 많다.


3. 왜 조회를 따로 분리하는가

회원가입 같은 기능은 엔티티 중심으로 다루는 것이 자연스럽다.
하지만 조회는 꼭 엔티티 전체가 필요하지 않을 때가 많다.

예를 들어 사용자 목록을 보여줄 때

  • 아이디만 필요할 수도 있고
  • 권한만 필요할 수도 있고
  • 상세 페이지가 아니라면 비밀번호나 불필요한 컬럼은 필요 없다

이럴 때 조회 전용 DTO로 필요한 값만 꺼내오면 더 효율적이다.
즉, 조회는 “저장 구조”보다 “보여줄 구조”가 더 중요하다.


4. 조회 API 흐름

구현 흐름은 다음처럼 정리할 수 있다.

  1. 클라이언트가 사용자 조회 API를 요청한다.
  2. 컨트롤러가 요청을 받는다.
  3. 서비스가 조회 로직을 수행한다.
  4. 서비스는 MyBatis Mapper를 호출한다.
  5. Mapper가 SQL을 실행한다.
  6. 조회 결과를 DTO로 받아 응답 객체에 담아 반환한다.

이 구조는 이전에 만든 로그인 흐름과 연결된다.
로그인이 끝난 뒤에는 JWT 인증 필터가 인증 정보를 SecurityContext에 넣어주고,
조회 API에서는 그 인증 정보를 꺼내서 “지금 로그인한 사용자가 누구인지” 알아낸다.


5. 내 정보 조회와 @AuthenticationPrincipal

내 정보 조회 API에서는 @AuthenticationPrincipal을 사용했다.

이 어노테이션은 Spring Security가 보관 중인 인증 객체에서
현재 로그인한 사용자의 정보를 꺼내 파라미터로 넣어준다.

즉, 별도로 username을 다시 요청받지 않아도 된다.

동작 흐름은 다음과 같다.

  1. 로그인 후 AccessToken을 발급받는다.
  2. 요청 시 헤더에 AccessToken을 담아 보낸다.
  3. JWT 필터가 토큰을 검증한다.
  4. 토큰에서 username을 꺼낸다.
  5. 사용자 정보를 불러와 인증 객체를 만든다.
  6. 이 인증 객체가 SecurityContext에 저장된다.
  7. @AuthenticationPrincipal이 그 안의 사용자 정보를 꺼낸다.

결국 @AuthenticationPrincipal은
“로그인한 사람 기준으로 조회해야 하는 API”에서 매우 자주 쓰인다.

예를 들면

  • 내 정보 조회
  • 내 주문 조회
  • 내 장바구니 조회

같은 기능에 자연스럽게 연결된다.


6. 관리자 전용 조회와 @PreAuthorize

사용자 목록 조회는 관리자만 가능하도록 제한했다.
여기서 사용한 것이 @PreAuthorize다.

@PreAuthorize("hasAuthority('ADMIN')")

이 의미는 메서드가 실행되기 전에
현재 인증된 사용자가 ADMIN 권한을 가지고 있는지 먼저 검사하겠다는 뜻이다.

즉,

  • 권한이 있으면 메서드 실행
  • 권한이 없으면 실행 전에 차단

된다.

이 방식은 URL 설정만으로는 부족할 때 특히 유용하다.

예를 들어

  • 어떤 URL은 같지만 파라미터에 따라 권한 체크가 다를 수 있고
  • 메서드별로 더 세밀하게 통제하고 싶을 수 있다

이럴 때 메서드 단위 보안이 더 직관적이다.


7. @PreAuthorize와 URL 보안 설정의 차이

Spring Security에서는 보통 두 군데에서 보안을 건다.

1. URL 단위 보안

  • 특정 URL 패턴에 대해 접근 허용/제한
  • 예: /admin/**는 관리자만 허용

2. 메서드 단위 보안

  • 실제 메서드 실행 전 조건 검사
  • 예: @PreAuthorize("hasAuthority('ADMIN')")

URL 보안은 전체적인 출입문 같은 느낌이고,
메서드 보안은 세부 방 안쪽 잠금장치 같은 느낌이다.

실무에서는 둘을 같이 쓰는 경우가 많다.


8. 조회 전용 DTO를 사용하는 이유

조회에서 엔티티를 그대로 반환하지 않고 DTO를 따로 둔 이유도 중요하다.

엔티티를 그대로 반환하면 생길 수 있는 문제

  • 불필요한 필드가 함께 노출될 수 있다
  • 응답 구조가 DB 구조에 종속된다
  • 화면 요구사항이 바뀔 때 유연하게 대응하기 어렵다

DTO를 사용하면 좋은 점

  • 필요한 값만 담을 수 있다
  • 응답 구조를 명확하게 제어할 수 있다
  • API 응답과 DB 저장 구조를 분리할 수 있다

조회는 특히 “무엇을 저장했는가”보다
“무엇을 보여줄 것인가”가 더 중요하기 때문에 DTO 분리가 더 의미 있다.


9. MyBatis를 같이 사용하는 이유

이번 구성은 변경 작업은 JPA, 조회 작업은 MyBatis로 나눠 이해하기 좋았다.

JPA가 잘 맞는 경우

  • 저장
  • 수정
  • 삭제
  • 엔티티 중심 처리
  • 트랜잭션 중심 작업

MyBatis가 잘 맞는 경우

  • 복잡한 조회
  • 원하는 컬럼만 조회
  • SQL을 직접 보면서 제어하고 싶을 때
  • 화면 맞춤형 응답이 필요할 때

즉, 둘 중 하나만 무조건 써야 하는 것이 아니라
목적에 따라 더 잘 맞는 도구를 선택하는 것이 핵심이다.


10. Mapper와 XML의 역할

MyBatis에서는 Mapper 인터페이스와 XML이 함께 동작한다.

Mapper 인터페이스

  • 자바에서 호출할 메서드를 정의
  • 예: 사용자 한 명 조회, 사용자 목록 조회

XML

  • 실제 SQL 작성
  • 어떤 컬럼을 어떤 조건으로 조회할지 명시

이 구조는 SQL이 눈에 보여서 이해하기 쉽다.
특히 조회 쿼리가 복잡해질수록 “무슨 SQL이 실행되는지” 명확하게 드러난다.


11. 설정에서 확인한 포인트

조회 기능을 붙이면서 설정도 같이 바뀌었다.

ddl-auto: update

  • 실행할 때마다 테이블을 새로 만드는 방식이 아니라
  • 기존 구조를 유지하며 변경분만 반영하는 쪽으로 설정

MyBatis 설정

  • 카멜 케이스 매핑 지원
  • 매퍼 XML 위치 지정
  • DTO 패키지 별칭 설정

이런 설정은 “기능 코드”는 아니지만 실제로는 매우 중요하다.
설정이 맞지 않으면 Mapper가 아예 동작하지 않거나, 컬럼 매핑이 꼬일 수 있다.


12. 인증과 조회가 연결되는 방식

이번 흐름에서 중요한 건 “인증이 끝나야 조회도 가능하다”는 점이다.

예를 들어 내 정보 조회는 다음처럼 이어진다.

  1. 로그인 성공
  2. AccessToken 발급
  3. 요청 시 Authorization 헤더에 토큰 포함
  4. JWT 필터가 토큰 검증
  5. 인증 객체 생성
  6. 컨트롤러에서 @AuthenticationPrincipal로 사용자 정보 사용
  7. 사용자명 기준으로 조회 실행

즉, 조회 기능이 따로 있는 것처럼 보여도
실제로는 앞단의 인증 구조 위에서 동작하는 기능이다.


13. 핵심 정리

  1. CQRS는 데이터를 변경하는 작업과 조회하는 작업을 분리하는 방식이다.
  2. 변경은 JPA, 조회는 MyBatis처럼 역할에 따라 기술을 나눌 수 있다.
  3. 조회에서는 엔티티보다 DTO 중심 설계가 더 유리한 경우가 많다.
  4. @AuthenticationPrincipal은 현재 로그인한 사용자 정보를 꺼낼 때 사용한다.
  5. 내 정보 조회는 로그인한 사용자 기준으로 동작하는 대표적인 예시다.
  6. @PreAuthorize는 메서드 실행 전에 권한을 검사하는 방식이다.
  7. 관리자 전용 기능은 메서드 단위 보안으로 더 명확하게 제어할 수 있다.
  8. MyBatis는 필요한 컬럼만 조회하거나 SQL을 직접 제어하고 싶을 때 강점이 있다.
  9. 인증 구조와 조회 구조는 분리되어 보이지만 실제로는 서로 연결되어 있다.
  10. 보안, 조회 DTO, SQL 분리까지 함께 고려해야 API 구조가 더 깔끔해진다.