C
Spring Boot/운영/Lesson 10

운영 핵심 — @Transactional · Spring Security

60분·theory

운영 핵심 — @Transactional · Spring Security

🎯 이 lesson 을 읽고 나면

이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.

  • ✅ Spring Security Filter Chain 그림 + JWT 인증 필터 위치
  • ✅ @PreAuthorize 로 메서드 레벨 권한 제어
  • ✅ BCryptPasswordEncoder 가 표준인 이유 (MD5/SHA-1 금지)

학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.

@Transactional — *Spring 의 가장 강력한 한 줄*

핵심 한 줄

@Transactional 어노테이션 한 줄이 해당 메서드를 DB 트랜잭션 안에서 실행합니다. 예외 발생 시 자동 롤백, 성공 시 commit. 송금·결제처럼 원자성이 중요한 작업의 표준.

가장 단순한 사용

java
@Service
public class TransferService {
    @Transactional
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepo.findById(fromId).get();
        Account to   = accountRepo.findById(toId).get();
        from.withdraw(amount);
        to.deposit(amount);
        // 메서드 끝 → commit
        // 예외 → 자동 rollback
    }
}

DB 의 BEGIN·COMMIT·ROLLBACK 를 Spring 이 자동 으로 감싸줍니다. 깜빡 잊을 일 없음.

프록시 기반 — 외부 호출만 적용

Spring 의 @TransactionalAOP 프록시 로 동작합니다. 메서드 외부에서 호출 될 때만 트랜잭션이 적용됩니다.

java
@Service
public class OrderService {
    @Transactional
    public void create() {
        process();        // ❌ 내부 호출 — 트랜잭션 적용 안 됨!
    }

    @Transactional
    public void process() { ... }
}

같은 클래스 안에서 메서드 호출은 프록시를 우회 → 어노테이션 무시.

해결:

  • 다른 Bean 으로 분리
  • 또는 AopContext.currentProxy() 로 자기 프록시 호출

전파 (Propagation) — 트랜잭션 안에서 또 트랜잭션

java
@Service
public class OrderService {
    @Transactional
    public void create() {
        logService.save();     // logService 도 @Transactional 이면?
    }
}

이미 트랜잭션 안인데 또 @Transactional 메서드를 호출하면 어떻게 될까요? 전파 옵션 으로 결정됩니다.

옵션의미
REQUIRED (기본)기존 있으면 합류, 없으면 새로 시작
REQUIRES_NEW항상 새 트랜잭션 시작 (기존 정지)
NESTED기존 안에 savepoint — 부분 롤백 가능
MANDATORY기존 있어야 함 (없으면 에러)
SUPPORTS기존 있으면 합류, 없으면 트랜잭션 없이
NEVER기존 있으면 에러

가장 흔한 사용: 기본 REQUIRED. 단, 로그·감사 기록 처럼 부모 롤백돼도 기록은 남아야 할 때는 REQUIRES_NEW.

java
@Transactional(propagation = REQUIRES_NEW)
public void writeAuditLog(...) { ... }

롤백 규칙 — RuntimeException 만 자동

java
@Transactional
public void process() {
    riskyOperation();      // throws IOException (Checked)
}

기본: RuntimeException 과 그 자손 (Unchecked) 만 자동 롤백. IOException 같은 Checked 예외는 commit 됩니다.

이게 Java 만의 특이한 디자인 — 다른 언어엔 거의 없는 구분. 영문 모르고 데이터 손상 사고가 잦습니다.

해결:

java
@Transactional(rollbackFor = Exception.class)
public void process() throws IOException { ... }

모든 예외에 롤백. 보통 이게 안전한 기본값. Kotlin·최근 Spring 코드는 거의 항상 명시.

readOnly = true — 읽기 전용 최적화

java
@Transactional(readOnly = true)
public List<User> findAll() { ... }

조회만 하는 메서드라면 readOnly=true. JPA 가 변경 감지를 건너뛰어 성능 향상. Hibernate FlushMode 도 자동 NEVER.

클래스 단위:

java
@Service
@Transactional(readOnly = true)        // 기본은 readOnly
public class UserService {
    public User findById(Long id) { ... }

    @Transactional                      // 쓰기 메서드만 override
    public User update(Long id, ...) { ... }
}

CQRS 패턴과 자연스럽게 맞닿습니다.

흔한 함정 5가지

1. 내부 호출 — 위에 설명. 같은 클래스 메서드 호출은 트랜잭션 무시
2. Checked 예외 commitrollbackFor = Exception.class 명시
3. private 메서드 — 프록시 적용 X. public 이어야
4. 트랜잭션 안에서 외부 API 호출 — DB 락이 외부 응답 시간만큼 잡힘. 트랜잭션 에서 호출 권장
5. 너무 큰 트랜잭션 — 전체 메서드 감싸면 락 오래. 최소 범위

한 번 정리

  • @Transactional 한 줄로 자동 트랜잭션
  • 프록시 기반 → 외부 호출만 적용
  • 전파 옵션 으로 중첩 동작 제어
  • 기본은 RuntimeException 만 롤백 → rollbackFor = Exception.class 권장
  • 읽기 메서드는 readOnly = true

Spring Security — *인증과 인가*

인증 vs 인가

자주 혼동되는 두 단어:

  • 인증 (Authentication) = 너 누구야? 신원 확인 (로그인)
  • 인가 (Authorization) = 너 할 수 있어? 권한 확인 (관리자 페이지 접근)

Spring Security 는 둘 다 처리합니다.

최소 설정 (Spring Security 6 / Spring Boot 3)

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())           // SPA·API 면 보통 disable
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

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

URL 별 권한, 세션 정책, JWT 필터 등록 — 모두 한 chain 에서.

로그인 흐름 — 가장 흔한 패턴

1. 비번 검증 (Authentication Manager)

java
@RequiredArgsConstructor
public class LoginService {
    private final UserRepository userRepo;
    private final PasswordEncoder encoder;
    private final JwtProvider jwt;

    public LoginResponse login(String email, String password) {
        User u = userRepo.findByEmail(email)
            .orElseThrow(() -> new BadCredentialsException("이메일 없음"));

        if (!encoder.matches(password, u.getPassword())) {
            throw new BadCredentialsException("비밀번호 불일치");
        }

        String accessToken  = jwt.createAccess(u.getId(), u.getRole());
        String refreshToken = jwt.createRefresh(u.getId());

        return new LoginResponse(accessToken, refreshToken);
    }
}

핵심: bcrypt 로 비번 해시 비교. 절대 평문 X.

2. JWT 발급 + 응답

  • Access Token — 15분, API 호출마다 헤더
  • Refresh Token — 7일, httpOnly 쿠키 (XSS 방어)

3. 이후 요청 — JwtAuthenticationFilter

java
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtProvider jwt;

    @Override
    protected void doFilterInternal(HttpServletRequest req, ...) {
        String token = extractToken(req.getHeader("Authorization"));
        if (token != null && jwt.validate(token)) {
            Authentication auth = jwt.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        chain.doFilter(req, res);
    }
}

매 요청마다 토큰 검증 → SecurityContext 에 사용자 정보 저장 → 다른 곳에서 @AuthenticationPrincipal 로 접근.

메서드 단위 인가

java
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/users/{id}")
public void delete(@PathVariable Long id) { ... }

@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
@PutMapping("/users/{userId}")
public UserDto update(@PathVariable Long userId, ...) { ... }

URL 단위가 아닌 메서드 단위 권한. SpEL (Spring Expression Language) 로 복잡한 규칙 표현 가능.

활성화: @EnableMethodSecurity 클래스에.

OAuth 2.0 — 소셜 로그인

직접 구현 X, Spring Security OAuth2 Client 사용:

yaml
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope: email,profile
          kakao:
            client-id: ...

Spring 이 전체 OAuth 흐름 (리다이렉트·토큰 교환·사용자 정보 조회) 을 처리. 콜백에서 우리 DB 의 사용자 와 매핑만.

흔한 보안 함정

1. CSRF disable 남용 — REST API + JWT 면 OK. 세션 쿠키 기반이면 enable 필요
2. CORS 너무 관대* 금지. 명시적 origin 리스트
3. 비번 평문 저장 — 사고 직전. bcrypt·argon2 필수
4. JWT secret 노출 — 환경변수, 절대 하드코딩 X
5. SQL Injection — JPA·Parameterized query 항상

한 번 정리

  • Spring Security = 인증 + 인가의 표준
  • JWT 가 모던 트렌드 (Stateless·모바일 친화)
  • 비번 bcrypt, JWT secret 환경변수, CSRF·CORS 적절히
  • 메서드 단위 권한은 @PreAuthorize
  • 소셜 로그인은 OAuth2 Client 한 줄

Spring Security Filter Chain · JWT 필터 구현

Security 의 핵심 — 필터 체인

Spring Security 는 수십 개의 필터를 체인으로 연결 합니다. HTTP 요청이 들어오면 순서대로 모든 필터를 통과 한 뒤 컨트롤러로 도착.

code
[HTTP 요청]
    ↓
[SecurityContextPersistenceFilter]   ← 인증 정보 로딩
    ↓
[UsernamePasswordAuthenticationFilter]   ← 로그인 폼
    ↓
[BearerTokenAuthenticationFilter]   ← JWT (커스텀)
    ↓
[ExceptionTranslationFilter]   ← 인증 실패 처리
    ↓
[FilterSecurityInterceptor]   ← 권한 검사
    ↓
[DispatcherServlet → Controller]

JWT 인증을 추가하려면 체인에 우리 필터를 끼워넣어야 합니다.

JWT 필터 — OncePerRequestFilter 상속

java
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtTokenProvider provider;

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                     HttpServletResponse res,
                                     FilterChain chain) throws ServletException, IOException {
        String header = req.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            try {
                Authentication auth = provider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(auth);
            } catch (JwtException e) {
                log.debug("JWT 검증 실패: {}", e.getMessage());
            }
        }
        chain.doFilter(req, res);
    }
}

OncePerRequestFilter한 요청당 한 번 실행 보장. Spring 의 forward·include 로 중복 실행 방지.

SecurityFilterChain 등록

java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())                 // JWT 는 stateless → CSRF 불필요
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**", "/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();    // bcrypt — *절대로 MD5/SHA-1 쓰지 마세요*
    }
}

.addFilterBefore(...)원하는 위치에 필터 삽입.

메서드 레벨 권한 — @PreAuthorize

java
@EnableMethodSecurity   // SecurityConfig 에 추가

@RestController
class AdminController {

    @PreAuthorize("hasRole('ADMIN')")
    @DeleteMapping("/users/{id}")
    public void deleteUser(@PathVariable Long id) { ... }

    @PreAuthorize("#id == authentication.principal.id or hasRole('ADMIN')")
    @GetMapping("/users/{id}")
    public UserResponse get(@PathVariable Long id) { ... }
}

SpEL (Spring Expression Language)복잡한 권한 조건 표현 가능. "본인 아니면 ADMIN 만" 같은 패턴이 한 줄로.

정리

  • 필터 체인 — 요청 처리 파이프라인
  • OncePerRequestFilter — JWT 같은 커스텀 필터의 표준 베이스
  • @PreAuthorize — 컨트롤러·서비스 레벨 권한 제어
  • BCryptPasswordEncoder — 비밀번호는 무조건 bcrypt

면접에서 "Security 어떻게 적용해보셨어요?" 라고 물으면 이 4가지를 언급 할 수 있어야 합니다.

🤖 AI 에게 이렇게 요청해보세요

이 lesson 의 개념을 알면 AI 에게 구체적으로 지시할 수 있습니다. 막연한 "고쳐줘" 가 아니라 어휘를 가진 요청 — 그게 토큰 절약의 출발점입니다.

  • "이 SecurityConfig 에 JWT 인증 필터 (OncePerRequestFilter) 추가해줘"
  • "이 메서드를 @PreAuthorize('hasRole(ADMIN)') 로 보호해줘"
  • "BCryptPasswordEncoder 를 Bean 으로 등록해줘"

왜 이게 토큰을 줄이나

개념을 모를 땐 AI 답변을 받고도 "그게 뭐예요?" 를 다시 물어야 합니다. 그 "다시 물음" 이 토큰을 잡아먹습니다. 개념 한 번 익혀두면 대화가 한 번에 끝납니다.

다음 추천: Database
운영 핵심 — @Transactional · Spring Security - Spring Boot