2/11(수) 멘토링, Spring Security 로그인

dev_joo·2026년 2월 11일

코드 카타 26.02.11

if, else, for, while 등 제어문 내 실행문이 한 줄일때 {} 생략이 가능하다.
그러나 나중에 코드가 추가되는 경우가 있고,
제어문 범위를 명확히 하기 위해 중괄호는 사용하는것이 좋다.

class Solution {
    public double solution(int[] arr) {
        double answer = 0;
        for(int x : arr)
            answer+=x; // 묵시적 형 변환
        return answer/arr.length;
    }
}
public class Solution {
    public int solution(int n) {
        int answer = 0;
        for(; n>0; n/=10) //다른 변수 초기화 없이 n 사용
            answer+=n%10;
        return answer;
    }
}
class Solution {
    public long[] solution(int x, int n) {
        long[] answer = new long[n];
        for (int i = 0; i < n; i++) {
            answer[i] = (long) x * (i + 1); // 오버플로우
        }
        return answer;
    }
}

처음엔 x만큼 늘어나는 변수를 따로 두고 배열에 저장해줬는데 배열 인덱스로 계산 할 수 있었다.

int 계산 중 오버플로우가 나지 않도록 long 타입으로 형변환을 해줘야 한다.

사실 예전에 이런 문제를 풀 땐 1시간은 고민해야했는데

관련 문제는 아니었음에도 다른 알고리즘 문제를 풀다보니 실력이 늘은건가? 신기했다. (뇌의 가소성?ㅎㅎ)


멘토링

사전 질문 폼을 제출하고 이를 바탕으로 순차적으로 멘토링이 이루어졌다.
zep에 팀원들과 있으면 한 명 한 명 멘토링을 위해 불려갔다. (와 조금 무서웠다!ㅋㅋㅋ)

오늘 내 차례가 돌아와서 멘토링 실로 갔다.
내 사전 질문을 요약하자면 내가 뭘 모르는지 몰라서 무섭다. 질문을 통해서 학습 방향을 잡아주시면 좋겠다. 라는 것이었다.

그랬더니 진짜 멘토님께서 면접을 대비한 다양한 질문을 해주셨다. 옛날에 티비에서 봤던 울려라 골든벨 같았다.

질문을 정리하면 다음과 같았다.

백엔드 개발에 필요한 개념이 모두 포함되어있는 황금질문들이었다.😎✨

객체란 무엇일까요?
SOLID 원칙이란?
Repository에서 발생할 수 있는 N+1 문제는 무엇이며, 어떻게 해결할 수 있을까요?
Dirty Checking 이 무엇이고 어떻게 동작하는지 설명할 수 있나요 ?
JPA 영속성 컨텍스트와 Entity 생명주기에 대해 설명해주세요.
FetchType.Lazy와 FetchType.Eager 의 차이를 설명해주세요.
영속성 전이란 무엇인가요 ?
Spring Bean 의 사용 이유와 원리가 무엇인가요 ?...

답변들에 어버버하면서 떠오르는 대로 답하긴 했는데 머릿속에서 이걸 어디서 들었더라... 개념이 뒤죽박죽하면서
문장이 쉽게 완성되진 않았다.

멘토님은 내 설명을 듣고나서 공부를 많이 한 티가 난다면서 칭찬해 주셨다.
😭(헛된 노력이 아니었다니 다행이군요)

그러나 면접은 짧고 정확하게 답변해야한다고 하셨다.

나는 처음 회사 들어갈 때도 지금이랑 비슷했던것 같은데..
왜 말로 하면 문장이 안 만들어질까?

글은:

  • 떠오르는 대로 막 쓴다
  • 고친다
  • 정리한다
  • 다시 다듬는다

말은:

  • 생각 + 정리 + 말하기 가 동시에 이뤄져야한다.

그래서 면접형 답변 구조를 정해두는 것이 도움이 될것이라고 생각했다.

1️⃣ 한 줄 정의
2️⃣ 왜 필요한지
3️⃣ 어떻게 동작하는지 (핵심 메커니즘)

면접용 답변 정리

멘토님께서 내 답변을 듣고 피드백 해주신 내용을 바탕으로 면접 답변을 정리해봤다.
두고두고 보면서 개념을 문장으로 잘 정리해놔야겠다.

1️⃣ 객체란 무엇인가요?

객체는 상태와 행동을 함께 가진 하나의 개념 단위라고 생각합니다.
예를 들어 버스라면 노선이나 속도 같은 상태가 있고, 운행한다는 행동이 있습니다.
이렇게 현실의 대상을 추상화해서 만들고, 객체끼리는 메시지를 주고받으며 협력합니다.

2️⃣ N+1 문제는 무엇이고 어떻게 해결하나요?

N+1 문제는 연관된 데이터를 조회할 때 쿼리가 불필요하게 여러 번 나가는 문제입니다.
예를 들어 게시글 10개를 조회했는데, 작성자 정보를 가져오려고 쿼리가 10번 더 실행되는 경우입니다.
주로 지연 로딩에서 발생하고, Fetch Join이나 EntityGraph를 사용해서 한 번의 쿼리로 조회하도록 해결합니다.

3️⃣ Dirty Checking이란?

Dirty Checking은 영속 상태의 엔티티가 변경되었는지 JPA가 자동으로 감지하는 기능입니다.
트랜잭션이 커밋될 때 처음 상태와 현재 상태를 비교해서 변경이 있으면 update 쿼리를 날립니다.
그래서 개발자가 직접 update를 호출하지 않아도 됩니다.

4️⃣ 영속성 컨텍스트와 엔티티 생명주기

영속성 컨텍스트는 JPA가 엔티티를 관리하는 공간입니다.
엔티티는 처음에는 비영속 상태이고, persist나 save를 하면 영속 상태가 됩니다.
영속 상태에서는 변경 감지가 이루어지고, flush 시점에 DB에 반영됩니다.
detach를 하면 준영속 상태가 되고, remove를 하면 삭제 상태가 됩니다.

5️⃣ Lazy와 Eager의 차이

Lazy는 연관된 데이터를 실제로 사용할 때 조회하는 방식이고,
Eager는 처음 조회할 때부터 함께 가져오는 방식입니다.
보통은 성능을 위해 Lazy를 기본으로 사용하고, 필요한 경우에만 Fetch Join으로 한 번에 조회합니다.

6️⃣ 영속성 전이란?

영속성 전이는 부모 엔티티의 상태 변화를 자식 엔티티에도 함께 적용하는 기능입니다.
예를 들어 부모를 저장할 때 자식도 같이 저장되도록 설정할 수 있습니다.
연관 관계가 강하게 묶여 있을 때 주로 사용합니다.

7️⃣ Spring Bean이란 무엇인가요?

Spring Bean은 스프링 컨테이너가 생성하고 관리하는 객체입니다.
스프링은 IoC 컨테이너를 통해 객체를 직접 생성하지 않아도 되게 하고, 의존성 주입을 통해 필요한 객체를 자동으로 연결해 줍니다.
그래서 결합도가 낮아지고, 유지보수나 테스트가 쉬워집니다.

+SOLID 원칙

SOLID는 객체지향 설계를 더 유연하고 확장 가능하게 만들기 위한 5가지 설계 원칙입니다.
코드의 결합도를 낮추고, 변경에 유연한 구조를 만들기 위해 사용합니다.
결과적으로 유지보수성과 확장성을 높이기 위한 설계 원칙입니다.

1️⃣ S - 단일 책임 원칙 (SRP)

하나의 클래스는 하나의 책임만 가져야 한다는 원칙입니다.
예를 들어 회원 가입과 이메일 발송을 한 클래스에서 모두 처리하면 변경 이유가 여러 개가 되기 때문에 분리하는 것이 좋습니다.
이렇게 하면 수정 범위가 줄어들고 유지보수가 쉬워집니다.

2️⃣ O - 개방 폐쇄 원칙 (OCP)

확장에는 열려 있고, 수정에는 닫혀 있어야 한다는 원칙입니다.
새로운 기능을 추가할 때 기존 코드를 고치기보다는, 구현체를 추가하는 방식으로 확장해야 합니다.
보통 인터페이스나 추상 클래스를 활용해 구현합니다.

3️⃣ L - 리스코프 치환 원칙 (LSP)

자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다는 원칙입니다.
즉, 상속을 사용하더라도 기존 동작의 의미를 깨면 안 됩니다.
부모 타입으로 사용했을 때 예상과 다른 동작이 나오면 안 됩니다.

4️⃣ I - 인터페이스 분리 원칙 (ISP)

하나의 큰 인터페이스보다, 역할별로 나뉜 작은 인터페이스 여러 개가 좋다는 원칙입니다.
사용하지 않는 기능까지 구현하게 만들면 결합도가 높아지기 때문입니다.

5️⃣ D - 의존성 역전 원칙 (DIP)

상위 모듈은 하위 모듈의 구체 클래스가 아니라 추상화에 의존해야 한다는 원칙입니다.
구현체가 아니라 인터페이스에 의존하도록 설계하면, 구현이 바뀌어도 영향을 최소화할 수 있습니다.
스프링의 DI가 대표적인 예입니다.

추가로 드린 질문

취업을 위해 공부하고 있는 상태라 어떤 개발자가 되어야 할지
미리 모델을 만들고 있으면 공부하는 데 도움이 될 것 같아 다음 질문을 드렸다.

선배 개발자가 봤을 때, 뽑고 싶은 개발자는 어떤 개발자인가요?

이야기가 잘 통하는, 의사소통이 잘 되는 개발자입니다.
용어의 개념을 정확히 이해하고 그 용어를 잘 사용해서 소통할 수 있는 개발자라면 좋겠습니다.

추가로 판교 사투리 예를 들어 설명해주기도 하셨다.
처음에 나는 판교 사투리가 뉴스에도 나오고 충분히 우리 말로도 대체할 수 있는 용어가 있으니 유난이라고 생각했는데
빠른 의사소통을 위해 미묘한 뉘앙스까지 고려해 선택된 단어가 남게 된것이라 생각하니 웃프다..😂

Spring 입문 강의

멘토링 때도 강의 진도가 안나가서 고민이라고 했지만 많이 들었다고 해주셨다.
캠프 전 제출한 작성한 코드를 바탕으로 선발 된 것이기 때문에 현재 실력에 대한 고민은 크게 하지 않아도 된다며 자신감을 불어넣어주셨다.
어쩌나 저쩌나 오늘은 오늘의 진도를 나가자!

Spring Security 로그인

SecurityConfig

package com.sparta.springauth.config;

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        // 로그인 사용
        http.formLogin((formLogin) ->
                formLogin
                        // 로그인 View 제공 (GET /api/user/login-page)
                        .loginPage("/api/user/login-page")
                        // 로그인 처리 (POST /api/user/login)
                        .loginProcessingUrl("/api/user/login")
                        // 로그인 처리 후 성공 시 URL
                        .defaultSuccessUrl("/")
                        // 로그인 처리 후 실패 시 URL
                        .failureUrl("/api/user/login-page?error")
                        .permitAll()
        );

        return http.build();
    }
}

SecurityConfig에서 HttpSecurityformLogin 설정을 통해 로그인 페이지 URL과 로그인 처리 방식을 설정한다.

// formLogin은 로그인 페이지 URL과 로그인 처리 방식을 설정한다.

formLogin.loginPage("로그인 페이지 URL")
// 인증되지 않은 사용자가 접근 시 리다이렉트 될 로그인 페이지 URL

formLogin.loginProcessingUrl("Spring Security가 가로채서 로그인 처리하는 URL")
// 해당 URL로 POST 요청이 오면 Security가 인증 처리

formLogin.defaultSuccessUrl("로그인 성공 후 기본 이동 경로")
// 기본값(false): 원래 요청한 페이지가 있으면 그쪽으로 이동

formLogin.defaultSuccessUrl("로그인 성공 후 이동 경로", true)
// true: 항상 이 URL로 이동 (원래 요청 무시)

formLogin.failureUrl("로그인 실패 시 이동 경로")

formLogin.permitAll()
// loginPage와 loginProcessingUrl은 인증 없이 접근 허용

Principal


Spring Security는 인증이 완료되면
SecurityContextAuthentication 객체를 저장한다.
이 객체 안에 있는 Principal이 바로 로그인 사용자 정보이다.

SecurityContext
 └── Authentication
      └── Principal (UserDetailsImpl)

이 때, Spring이 인증 정보를 저장한다길래 JWT의 무상태를 이용하는게 맞는가? 라는 의문이 들었다.

하지만 SecurityContext의 경우,
ThreadLocal (현재 요청을 처리하는 쓰레드 내부) 에 위치한 SecurityContextHolder에 저장해 두었다가

앞선 필터 체인이 종료되고 SecurityContextPersistenceFilter가 Context를 비우고 thread를 반환 하기 때문에 서버 어딘가에 계속 상태를 저장하고 있는 것이 아니다.

한 번의 요청 동안만 유지되는 것이기 때문에 JWT가 추구하는 무상태를 지킨 것이 맞다.

Controller

@AuthenticationPrincipal
SecurityContext의 Authentication.getPrincipal() 값을
바로 꺼내서 주입해주는 어노테이션으로,
파라미터로 principal 을 받아 올 수 있다.
여기서 principal은 UserDetails의 구현체로 저장되어있다.

@Controller
@RequestMapping("/api")
public class ProductController {

    @GetMapping("/products")
    public String getProducts(@AuthenticationPrincipal UserDetailsImpl userDetails) {
        User user = userDetails.getUser();
        System.out.println("user.getUsername() = " + user.getUsername());
        return "redirect:/";
    }
}

UserDetails

UserDetails는 username, password, 권한, 계정의 상태가 정의된 인터페이스다.
pring Security는 로그인 시 이 인터페이스를 기준으로 인증을 처리한다.

package org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.Collection;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.GrantedAuthority;

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    @Nullable String getPassword();

    String getUsername();

    default boolean isAccountNonExpired() {
        return true;
    }

    default boolean isAccountNonLocked() {
        return true;
    }

    default boolean isCredentialsNonExpired() {
        return true;
    }

    default boolean isEnabled() {
        return true;
    }
}

UserDetailsImpl

UserDetails를 구현한 UserDetailsImpl은 Principal로 저장되고 인증 과정에 사용된다.

package com.sparta.springauth.security;

public class UserDetailsImpl implements UserDetails {

    private final User user;

    public UserDetailsImpl(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRoleEnum role = user.getRole();
        String authority = role.getAuthority();

        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);

        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

UserDetailService

UserDetailsService는 Spring Security에서 제공하는 인터페이스로,
로그인 시 사용자 정보를 조회하는 역할을 한다.

loadUserByUsername(String username) 메서드는
사용자가 입력한 username을 기준으로 사용자 정보를 조회하고, 그 정보를 UserDetails 객체로 반환한다.

public interface UserDetailsService {

	/**
	loadUserByUsername(String username)

	전달받은 username으로 사용자를 조회한다.

	구현 방식에 따라 대소문자 구분 여부는 달라질 수 있다.

	조회된 결과는 UserDetails 객체로 반환해야 한다.

	절대 null을 반환하면 안 된다.

	사용자가 없거나 권한(GrantedAuthority)이 없다면
	→ UsernameNotFoundException을 발생시켜야 한다.
	*/
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

UserDetailsServiceImpl

UserDetailsService의 구현체로
Repository를 이용해 사용자가 입력한 username으로 해당 유저를 조회하고 검증한다.

package com.sparta.springauth.security;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

        return new UserDetailsImpl(user);
    }
}

반환된 UserDetails는 이후 비밀번호 검증 과정을 거쳐
인증이 성공하면 Authentication 객체가 생성되고,
이때 해당 UserDetails 객체가 principal로 저장된다.

SpringSecurity 로그인 흐름 정리

1️⃣ 로그인 요청
2️⃣ UserDetailsService.loadUserByUsername(username) 호출
3️⃣ UserRepository에서 사용자 조회
4️⃣ UserDetailsImpl 생성
5️⃣ Spring Security가 비밀번호 검증
6️⃣ 인증 성공 시 Authentication 생성
7️⃣ Authentication 안에 principal로 UserDetailsImpl 저장
8️⃣ SecurityContextAuthentication 저장
9️⃣ 컨트롤러에서 @AuthenticationPrincipal로 꺼내 사용

인증 과정 중 예외가 발생하면 Authentication 객체가 생성되지 않으며, SecurityContext에 저장되지 않는다.

로그아웃은

SecurityContextHolder.clearContext();

어제 강의에서 Security 부분이 전혀 이해가 안 가서 걱정이 됐는데
시간을 들여 객체 사이의 흐름을 유심히 보니 조금은 이해가 되는것 같아 마음이 놓였다.

profile
풀스택 연습생. 끈기있는 삽질로 무대에서 화려하게 데뷔할 예정 ❤️🔥

0개의 댓글