멀티캠퍼스

[2026.07.09] - TIL 69일차 (Spring Security와 로그인 구현)

buckwheat 2026. 7. 9. 17:58

1. Spring Security 의존성 추가

Spring Boot 프로젝트에 로그인과 접근 권한 기능을 적용하기 위해 Spring Security 의존성을 추가했다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

spring-boot-starter-security는 인증과 권한 처리를 담당한다.

thymeleaf-extras-springsecurity5는 Thymeleaf 화면에서 로그인 정보와 권한을 사용할 수 있게 해준다.


2. 게시판 Entity와 JPA

게시글 정보를 저장하기 위해 BoardEntity를 작성했다.

@Entity
@Table(name = "board")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class BoardEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(nullable = false, length = 50)
    private String write;

    @Column(nullable = false, length = 100)
    private String title;

    @Column(length = 4000)
    private String content;

    @Column(updatable = false)
    private LocalDate createAt;

    @PrePersist
    public void onPrePersist() {
        this.createAt = LocalDate.now();
    }
}
  • @Entity: JPA가 관리하는 Entity로 등록
  • @Table: 연결할 테이블 이름 지정
  • @Id: 기본키 지정
  • @GeneratedValue: 기본키 자동 생성
  • @PrePersist: INSERT 직전에 실행
  • Lombok: 생성자와 getter, setter 코드 자동 생성

3. Repository와 Service

Repository는 JpaRepository를 상속받아 기본 CRUD 기능을 사용한다.

public interface BoardRepository
        extends JpaRepository<BoardEntity, Integer> {

    List<BoardEntity> findAllByOrderByIdDesc();
}

메서드 이름을 분석해 게시글을 ID 내림차순으로 조회하는 SQL을 자동 생성한다.

Service에서는 Repository를 이용해 게시글 조회와 저장을 처리했다.

@Override
public List<BoardEntity> findAll() {
    return repository.findAllByOrderByIdDesc();
}

@Override
public BoardEntity save(BoardEntity dto) {
    return repository.save(dto);
}

4. 게시글 목록, 등록, 상세 조회

Controller에서 게시글 목록, 등록 화면, 저장, 상세 조회 요청을 처리했다.

@GetMapping("/list")
public String selectList(Model model) {
    model.addAttribute("list", service.findAll());
    return "board/list";
}

@GetMapping("/insert")
public String insertForm() {
    return "board/insert";
}

@PostMapping("/insert")
public String insert(BoardEntity dto) {
    service.save(dto);
    return "redirect:/board/list";
}

@GetMapping("/detail")
public String selectOne(Model model, int id) {
    model.addAttribute("dto", service.findById(id));
    return "board/detail";
}
  • @GetMapping: 화면 조회 요청
  • @PostMapping: 입력 데이터를 서버에 전달하는 요청
  • Model: Controller에서 조회한 데이터를 Thymeleaf로 전달
  • redirect:: 작업 완료 후 새로운 주소로 다시 요청

5. 회원 Entity와 회원가입

회원 정보를 저장하기 위해 MemberEntity를 작성했다.

@Entity
@Table(name = "member")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MemberEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(nullable = false, length = 50, unique = true)
    private String username;

    @Column(nullable = false, length = 100)
    private String password;

    @Column(nullable = false, length = 50)
    private String name;
}

username에는 unique = true를 지정해 같은 아이디가 중복 저장되지 않도록 했다.


6. BCrypt 비밀번호 암호화

비밀번호를 평문으로 저장하지 않도록 BCryptPasswordEncoder를 Bean으로 등록했다.

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

회원가입 시 입력받은 비밀번호를 암호화한 뒤 DB에 저장한다.

@Override
public MemberEntity save(MemberEntity dto) {
    dto.setPassword(passwordEncoder.encode(dto.getPassword()));
    return repository.save(dto);
}

BCrypt로 암호화된 비밀번호는 약 60자의 문자열로 저장된다.


7. UserDetailsService

Spring Security는 로그인 요청이 들어오면 loadUserByUsername()을 호출해 사용자를 조회한다.

@Override
public UserDetails loadUserByUsername(String username)
        throws UsernameNotFoundException {

    MemberEntity member = repository.findByUsername(username)
        .orElseThrow(() ->
            new UsernameNotFoundException("사용자를 찾을 수 없습니다."));

    return User.builder()
        .username(member.getUsername())
        .password(member.getPassword())
        .roles("USER")
        .build();
}

DB에서 조회한 MemberEntity를 Spring Security가 사용하는 UserDetails 형태로 변환한다.


8. SecurityFilterChain

SecurityFilterChain에서 URL별 접근 권한과 로그인 방식을 설정했다.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
        throws Exception {

    http.csrf(csrf -> csrf.disable())
        .authorizeHttpRequests(auth -> auth
            .antMatchers(
                "/",
                "/member/register",
                "/member/login",
                "/css/**",
                "/js/**",
                "/images/**"
            ).permitAll()
            .antMatchers("/board/**").authenticated()
            .anyRequest().permitAll())
        .formLogin(form -> form
            .loginPage("/member/login")
            .loginProcessingUrl("/member/login")
            .defaultSuccessUrl("/board/list", true)
            .failureUrl("/member/login?error=true")
            .permitAll())
        .logout(logout -> logout
            .logoutUrl("/member/logout")
            .logoutSuccessUrl("/")
            .permitAll());

    return http.build();
}
  • permitAll(): 로그인하지 않아도 접근 가능
  • authenticated(): 로그인한 사용자만 접근 가능
  • loginPage(): 로그인 화면 주소
  • loginProcessingUrl(): Spring Security가 처리할 로그인 요청 주소
  • defaultSuccessUrl(): 로그인 성공 후 이동할 주소
  • failureUrl(): 로그인 실패 후 이동할 주소

9. 로그인 처리 흐름

로그인 폼 제출
→ Spring Security Filter
→ loadUserByUsername() 호출
→ DB에서 회원 조회
→ BCrypt 비밀번호 비교
→ 인증 성공
→ /board/list 이동

Spring Security가 POST /member/login 요청을 처리하기 때문에 Controller에서 아이디와 비밀번호를 직접 비교하지 않아도 된다.

Controller는 로그인 화면을 반환하는 GET 요청만 처리한다.

@GetMapping("/login")
public String loginForm() {
    return "member/login";
}

10. ddl-auto 설정

수업에서는 서버 실행 시 테이블을 새로 생성하기 위해 다음 설정을 사용했다.

spring.jpa.hibernate.ddl-auto=create

create는 서버를 실행할 때 기존 테이블을 삭제하고 다시 생성한다. 이 때문에 서버를 재시작하면 회원과 게시글 데이터가 사라질 수 있다.

실습 중 데이터를 유지하기 위해 다음과 같이 변경했다.

spring.jpa.hibernate.ddl-auto=update

update는 기존 데이터를 유지하면서 Entity 변경 사항을 테이블에 반영한다.


정리

오늘은 Spring Data JPA로 게시판과 회원 테이블을 구성하고, Spring Security를 이용해 회원가입과 로그인 기능을 구현했다.

비밀번호는 BCrypt로 암호화했으며, UserDetailsService로 DB 회원을 조회했다. 또한 로그인하지 않은 사용자는 /board/**에 접근할 수 없도록 URL 권한을 설정했다.