๐Ÿš€ Spring Boot ๋ณด์•ˆ ์„ค์ • ์ •๋ฆฌ

Kim Chanwooยท2025๋…„ 9์›” 26์ผ

Spring Boot

๋ชฉ๋ก ๋ณด๊ธฐ
5/6
post-thumbnail

CORS + Role ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด + ํ…Œ์ŠคํŠธ


๐ŸŒ CORS Filter ์„ค์ •

๐Ÿ”Ž CORS๋ž€?

  • CORS (Cross-Origin Resource Sharing)
    ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„๊ฐ€ ๋‹ค๋ฅธ ์ถœ์ฒ˜(Origin)์ผ ๋•Œ, ์š”์ฒญ์„ ํ—ˆ์šฉํ• ์ง€ ์ฐจ๋‹จํ• ์ง€๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ๋ฉ”์ปค๋‹ˆ์ฆ˜.
  • ๋ธŒ๋ผ์šฐ์ €๋Š” ๋ณด์•ˆ์ƒ ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ต์ฐจ ์ถœ์ฒ˜ ์š”์ฒญ์„ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค.
  • ๋ฐฑ์—”๋“œ ์„œ๋ฒ„์—์„œ CORS Filter๋ฅผ ์„ค์ •ํ•˜๋ฉด, ํ—ˆ์šฉ๋œ Origin์— ๋Œ€ํ•ด ์š”์ฒญ์„ ์—ด์–ด์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โš™๏ธ CORS Filter ์ถ”๊ฐ€

SecurityConfig ํด๋ž˜์Šค์— CorsConfigurationSource๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration();

    // ํ—ˆ์šฉํ•  Origin (ํŠน์ • ๋„๋ฉ”์ธ๋งŒ ํ—ˆ์šฉ ๊ฐ€๋Šฅ)
    config.setAllowedOrigins(Arrays.asList("http://localhost:5173","http://localhost:3000"));
    // ๋ชจ๋“  HTTP ๋ฉ”์„œ๋“œ ํ—ˆ์šฉ
    config.setAllowedMethods(Arrays.asList("*"));
    // ๋ชจ๋“  Header ํ—ˆ์šฉ
    config.setAllowedHeaders(Arrays.asList("*"));

    config.setAllowCredentials(false);
    config.applyPermitDefaultValues();

    source.registerCorsConfiguration("/**", config);
    return source;
}

โœ… * ๋กœ ์„ค์ •ํ•˜๋ฉด ๋ชจ๋“  Origin์„ ํ—ˆ์šฉํ•˜์ง€๋งŒ, ์‹ค์ œ ๋ฐฐํฌ ํ™˜๊ฒฝ์—์„œ๋Š” ํŠน์ • ๋„๋ฉ”์ธ๋งŒ ๋ช…์‹œํ•˜๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ”ง filterChain ์ˆ˜์ •

HttpSecurity ์„ค์ •์—์„œ .cors(withDefaults())๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf(csrf -> csrf.disable())
            .cors(withDefaults()) // โœ… cors ์„ค์ • ์ถ”๊ฐ€
            .sessionManagement(sessionManagement ->
                    sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers(HttpMethod.POST, "/login").permitAll()
                    .anyRequest().authenticated())
            .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling(exceptionHandling ->
                    exceptionHandling.authenticationEntryPoint(exceptionHandler));
    return http.build();
}

๐Ÿ”‘ Role-Based Protection (๊ถŒํ•œ ๋ถ„๋ฆฌ)

๐Ÿ“Œ AppUser Entity

  • ์œ ์ €๋Š” role ํ•„๋“œ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค. ("USER", "ADMIN")
  • Spring Security๋Š” hasRole() ๋ฉ”์„œ๋“œ๋กœ ํŠน์ • ์—”๋“œํฌ์ธํŠธ ์ ‘๊ทผ์„ ์ œํ•œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โš™๏ธ ์—”๋“œํฌ์ธํŠธ ๊ถŒํ•œ ์ œ์–ด

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf(csrf -> csrf.disable())
            .cors(withDefaults())
            .sessionManagement(sessionManagement ->
                    sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/admin/**").hasRole("ADMIN") // ๊ด€๋ฆฌ์ž ์ „์šฉ
                    .requestMatchers("/user/**").hasRole("USER")   // ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž ์ „์šฉ
                    .anyRequest().authenticated())
            .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling(exceptionHandling ->
                    exceptionHandling.authenticationEntryPoint(exceptionHandler));
    return http.build();
}

โœ… ๋ณดํ†ต์€ USER < MANAGER < ADMIN ํ˜•ํƒœ์˜ ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ“Œ ๋ฉ”์„œ๋“œ ์ˆ˜์ค€ ๊ถŒํ•œ ์ œ์–ด

ํด๋ž˜์Šค๋‚˜ ๋ฉ”์„œ๋“œ ์œ„์— ์• ๋„ˆํ…Œ์ด์…˜์„ ๋ถ™์—ฌ ์„ธ๋ฐ€ํ•˜๊ฒŒ ์ œ์–ดํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

  • @PreAuthorize("hasRole('ADMIN')")
  • @Secured("ROLE_USER")

โš ๏ธ ๊ธฐ๋ณธ ์„ค์ •์—์„œ๋Š” ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ @EnableMethodSecurity๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


๐Ÿงช Spring Boot Test

Spring Boot์—์„œ๋Š” spring-boot-starter-test๋กœ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
(JUnit, Mockito, AssertJ ๊ธฐ๋ณธ ํฌํ•จ)

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'com.h2database:h2'

๐Ÿ”ฌ ํ…Œ์ŠคํŠธ ์ข…๋ฅ˜

  1. ๋‹จ์œ„ ํ…Œ์ŠคํŠธ (Unit Test) โ†’ ๋ฉ”์„œ๋“œ ๋‹จ์œ„ ๊ฒ€์ฆ
  2. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (Integration Test) โ†’ ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ์ƒํ˜ธ์ž‘์šฉ ํ™•์ธ
  3. ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ (Functional Test) โ†’ ์š”๊ตฌ์‚ฌํ•ญ ์ถฉ์กฑ ์—ฌ๋ถ€ ๊ฒ€์ฆ
  4. ํšŒ๊ท€ ํ…Œ์ŠคํŠธ (Regression Test) โ†’ ์ˆ˜์ • ํ›„ ๊ธฐ์กด ๊ธฐ๋Šฅ ์ •์ƒ ๋™์ž‘ ์—ฌ๋ถ€ ํ™•์ธ
  5. ์ด์šฉ์„ฑ ํ…Œ์ŠคํŠธ (Usability Test) โ†’ UX ๊ด€์ ์—์„œ ๊ฒ€์ฆ

โœ… Repository ํ…Œ์ŠคํŠธ (CRUD ๊ฒ€์ฆ)

@DataJpaTest
class OwnerRepositoryTest {
    @Autowired
    private OwnerRepository ownerRepository;

    @Test
    @DisplayName("์‚ญ์ œ ํ…Œ์ŠคํŠธ")
    void deleteOwners() {
        ownerRepository.save(new Owner("ํŒ”๋ฐฑ", "๋ฐ•"));
        ownerRepository.deleteAll();
        assertThat(ownerRepository.count()).isEqualTo(0);
    }
}

โœ… Controller ํ…Œ์ŠคํŠธ (MockMvc ํ™œ์šฉ)

์‹ค์ œ ์„œ๋ฒ„๋ฅผ ๋„์šฐ์ง€ ์•Š๊ณ  HTTP ์š”์ฒญ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜.

@SpringBootTest
@AutoConfigureMockMvc
public class CarRestTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    @DisplayName("๋กœ๊ทธ์ธ ์ธ์ฆ ํ…Œ์ŠคํŠธ")
    public void testAuthentication() throws Exception {
        this.mockMvc.perform(post("/login")
                .content("{\"username\":\"admin\",\"password\":\"admin\"}")
                .header(HttpHeaders.CONTENT_TYPE, "application/json"))
                .andDo(print())
                .andExpect(status().isOk());
    }
}

โœ… CarRepository ์ €์žฅ ํ…Œ์ŠคํŠธ

@DataJpaTest
public class CarRepositoryTest {
    @Autowired
    private CarRepository carRepository;
    @Autowired
    private OwnerRepository ownerRepository;

    @Test
    @DisplayName("์ฐจ๋Ÿ‰ ์ €์žฅ ํ…Œ์ŠคํŠธ")
    void saveCar() {
        Owner owner = new Owner("Gemini", "GPT");
        ownerRepository.save(owner);

        Car car = new Car("Ford", "Mustang", "Red", "ABCEDF", 2021, 567890, owner);
        carRepository.save(car);

        assertThat(carRepository.findById(car.getId())).isPresent();
        assertThat(carRepository.findById(car.getId()).get().getBrand()).isEqualTo("Ford");
    }
}

๐Ÿ“Œ ์ •๋ฆฌ

  • ๐ŸŒ CORS Filter โ†’ ๊ต์ฐจ ์ถœ์ฒ˜ ์š”์ฒญ ์ œ์–ด (๋ณด์•ˆ + ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ํ—ˆ์šฉ)
  • ๐Ÿ”‘ Role-Based Protection โ†’ USER, ADMIN ์—ญํ• ์— ๋”ฐ๋ผ ์ ‘๊ทผ ๊ถŒํ•œ ๋ถ„๋ฆฌ
  • ๐Ÿงช ํ…Œ์ŠคํŠธ ์ฝ”๋“œ โ†’ Repository CRUD, Controller ์š”์ฒญ์„ ์‹ค์ œ์ฒ˜๋Ÿผ ๊ฒ€์ฆ

0๊ฐœ์˜ ๋Œ“๊ธ€