repository -> service -> controller 순으로 개발
service가 비즈니스 로직을 가지고 있기에 가장 시간이 많이 걸림.
한 개씩 개발하고 테스트 반복 / 1,2,3 다 개발하고 테스트 / 모든 테스트를 만들고 1,2,3 한 개씩 개발
package com.mycom.myapp.webapp.springboottest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.mycom.myapp.auth.controller.LoginController;
import com.mycom.myapp.auth.service.LoginService;
import com.mycom.myapp.user.dto.UserResultDto;
import com.mycom.myapp.user.entity.User;
import com.mycom.myapp.user.repository.UserRepository;
import jakarta.servlet.http.HttpSession;
@SpringBootTest
public class LoginTest {
// 1. repository test
@Autowired
private UserRepository userRepository;
@Test
public void testLoginRepository() {
Optional<User> optionalUser = userRepository.findByEmail("user3@mycom.com");
assertTrue(optionalUser.isPresent());
}
// 2. service test
@Autowired
private LoginService loginService;
@Test
public void testLoginService() {
UserResultDto userResultDto = loginService.login("user3@mycom.com","password3");
assertNotNull(userResultDto.getUserDto());
}
// 3. controller test
@Autowired
private LoginController loginController;
@Autowired
private HttpSession session;
@Test
public void testLoginController() {
UserResultDto userResultDto = loginController.login("user3@mycom.com","password3", session);
// userResultDto의 result가 success 확인
// session의 userDto 확인
assertEquals("success", userResultDto.getResult());
assertNotNull(session.getAttribute("userDto"));
}
}
package com.mycom.myapp.webapp.springboottest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.mycom.myapp.auth.controller.LoginController;
import com.mycom.myapp.auth.service.LoginService;
import com.mycom.myapp.user.controller.UserController;
import com.mycom.myapp.user.dto.UserResultDto;
import com.mycom.myapp.user.entity.User;
import com.mycom.myapp.user.repository.UserRepository;
import com.mycom.myapp.user.service.UserService;
import jakarta.servlet.http.HttpSession;
import jakarta.transaction.Transactional;
@SpringBootTest
public class RegisterTest {
// 1. repository test
@Autowired
private UserRepository userRepository;
// 테스트 과정에서 생긴 변화(insert, update, delete)를 모두 롤백
@Test
@Transactional
// 최초 홍길동 insert, @Transactional 추가 후 이길동 insert하면 테스트는 성공하고 데이터는 다시 롤백
// service에 @Transactional은 어떻게 영향을 받는가? @Test의 @Transactional 진행에 포함, 독자적으로 동작 x
// 추가 options 등을 통해서 다양한 설정이 있지만 복잡하고 도움이 되지 않아 추천 x
public void testLoginRepository() {
User user = new User();
user.setName("이길동");
user.setEmail("hong@gildon.com");
user.setPassword("1234");
userRepository.save(user);
User savedUser = userRepository.save(user); // db에 insert되고 영속화된 savedUser
assertNotNull(savedUser);
}
// 2. service test
@Autowired
private UserService userService;
@Test
@Transactional
public void testLoginService() {
User user = new User();
user.setName("이길동");
user.setEmail("hong@gildon.com");
user.setPassword("1234");
UserResultDto userResultDto = userService.insertUser(user);
assertEquals("success", userResultDto.getResult());
}
// 3. controller test
@Autowired
private UserController userController;
@Autowired
private HttpSession session;
@Test
@Transactional
public void testLoginController() {
User user = new User();
user.setName("이길동");
user.setEmail("hong@gildon.com");
user.setPassword("1234");
UserResultDto userResultDto = userController.insertUser(user);
assertEquals("success", userResultDto.getResult());
assertNotNull(session.getAttribute("userDto"));
}
}
인증(Authentication)과 인가(Authorization)의 표준화 -> 개발 실수 방지
강력한 보안 공격 방어 (CSRF, Session Fixation 등)
서블릿 필터(Servlet Filter) 단계에서 요청을 가로채서 검사
Spring Security 6은 상속을 버렸음


Spring Security


package com.mycom.myapp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
// #1
// 모든 요청에 대해 허락
// 가장 단순한 정책을 가진 SecurityFilterChain
// @Bean
// SecurityFilterChain fiterChain(HttpSecurity http) throws Exception{
// return http
// .authorizeHttpRequests(request -> request.anyRequest().permitAll())
// .build();
// }
// #2
// 모든 요청에 대해 인증 필요(authenticated())
// 인증이 필요한 설정 -> 로그인 방식 지정
// @Bean
// SecurityFilterChain fiterChain(HttpSecurity http) throws Exception{
// return http
// .authorizeHttpRequests(request -> request.anyRequest().authenticated())
// .formLogin(Customizer.withDefaults())
// .build();
// }
// #3
// 일부 요청에 대해 허락하고, 나머지 요청에 대해 인증 필요(authenticated())
// 인증이 필요한 설정 -> 로그인 방식 지정
@Bean
SecurityFilterChain fiterChain(HttpSecurity http) throws Exception{
return http
.authorizeHttpRequests(request -> {
request.requestMatchers("/","index.html").permitAll();
request.anyRequest().authenticated();
})
.formLogin(Customizer.withDefaults())
.build();
}
}
package com.mycom.myapp.user.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/customer")
public class CustomerController {
@GetMapping("/hello")
public String hello() {
return "Hello, Customer";
}
}
package com.mycom.myapp.user.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/hello")
public String hello() {
return "Hello, Admin";
}
}
package com.mycom.myapp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
// PasswordEncoder DI
// MyUserDetailsService 에서 UserDetails 객체를 생성하는 과정에서 필요.
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// #1.
// UserDetailsService 적용에 맞는 formLogin
// logout 추가
// Role 적용 X
// @Bean
// SecurityFilterChain filterChain(HttpSecurity http ) throws Exception{
// return http
// .authorizeHttpRequests( request -> {
// request.requestMatchers("/", "/index.html").permitAll(); // 일부 요청
// request.anyRequest().authenticated(); // 나머지 요청
// })
// .formLogin(
// form -> form.defaultSuccessUrl("/")
// .permitAll()
// )
// .logout(logout -> logout.permitAll()) // logout
// .build();
// }
// #2.
// Role 적용 O
// Authentication( 로그인 ) -> Authorization ( 동일하게 로그인했어도 어떤 Role 을 가졌느냐에 따라 다르게 권한 부여 ) 적용
// HttpSecyrity http 메소드 체인 적용. request -> {} {} 제거
@Bean
SecurityFilterChain filterChain(HttpSecurity http ) throws Exception{
return http
.authorizeHttpRequests( request -> request
.requestMatchers("/", "/index.html").permitAll() // 일부 요청
.requestMatchers("/customer/**").hasAnyRole("CUSTOMER", "ADMIN") // Role 기반 접근
.requestMatchers("/admin/**").hasRole("ADMIN") // Role 기반 접근
.anyRequest().authenticated() // 나머지 요청
)
.formLogin(
form -> form.defaultSuccessUrl("/")
.permitAll()
)
.logout(logout -> logout.permitAll()) // logout
.build();
}
}

package com.mycom.myapp.config;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@Service // Service Layer 중 한 개
@RequiredArgsConstructor
public class MyUserDetailsService implements UserDetailsService{
// Spring Security 는 사용자의 패스워드를 암호화.
// Spring Security 에 DI 요청
// DI 되는 BCryptPasswordEncoder
// SecurityConfig 에서 DI 설정
private final PasswordEncoder passwordEncoder;
// 기본 인증 대신 사용자별 인증을 구현
// parameter username 을 통해서 DB Access 통해 username 에 해당하는 User 의 기본 정보를 UserDetails 객체로 만들어서 리턴한다.
// DB Access X, 대신 dskim/1234 를 username, password 사용
// #1. Role 적용 전
// @Override
// public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//
// if("dskim".equals(username)) {
// return User.builder() // User 는 Spring Security 의 User
// .username("dskim")
// .password(passwordEncoder.encode("1234"))
// .build();
// } else {
// throw new UsernameNotFoundException("User Not Found");
// }
// }
// #2. Role 적용 후
// 사용자별 Role 은 DB Access 를 통해서 처리해야 함. 현재는 Role 도 하드코딩.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if("dskim".equals(username)) {
return User.builder() // User 는 Spring Security 의 User
.username("dskim")
.password(passwordEncoder.encode("1234"))
// .roles("CUSTOMER")
// .roles("ADMIN")
.roles("ADMIN", "CUSTOMER") // 현재 코드는 ADMIN 한 개만 있어도 CUSTOMER 에 접근가능한 상황. 많은 경우 1 사용자가 복수 개의 Role 을 일반적으로 가진다.
.build();
} else {
throw new UsernameNotFoundException("User Not Found");
}
}
}
개발 순서:
Repository (DB 접근) → Service (비즈니스 로직) → Controller (요청 처리)
- LoginTest, RegisterTest 예제처럼 각 계층(Layer)을 하나씩 테스트
- @SpringBootTest: 실제 스프링 환경을 띄워서 통합 테스트를 수행
- @Transactional (테스트용): 테스트가 끝나면 DB 데이터를 자동으로 롤백하여,
다음 테스트에 영향을 주지 않음
Spring Security 6 >
--SecurityConfig--
passwordEncoder
- BCryptPasswordEncoder를 @Bean으로 등록하여 암호화에 사용
- SecurityConfig에서 정의하고 UserDetailsService에서 사용
@Bean
SecurityFilterChain fiterChain(HttpSecurity http) throws Exception{
return http
.authorizeHttpRequests(request -> {
request.requestMatchers("/","index.html").permitAll();
request.anyRequest().authenticated();
})
.formLogin(Customizer.withDefaults())
.build();
}
- authorizeHttpRequests: URL별 접근 권한 설정
- requestMatchers(): 동일한 주소, HTTP 메서드 같이 설정 값만 통과
- permitAll(): 누구나 접근 가능
- authenticated(): 인증된 사람만 (로그인)
- formLogin(): 로그인 형태 제공
- Customizer.withDefaults(): Spring Security가 제공하는 기본 로그인 페이지와 설정
- logout(): 로그아웃 제공
- hasAnyRole(), hasRole(): role 여러개/한개 에 따른 권한 제공
-- MyUserDetailsService --
implements UserDetailsService: DB에 있는 회원 정보를 Spring Security에게 알려줌
=> UserDetails 객체 (암호화된 비밀번호)
- loadUserByUsername(): 로그인 시 입력한 ID(username)로 회원을 찾기
- User.builder(): 찾은 회원 정보를 스프링 시큐리티가 이해하는 UserDetails 객체로 변환하여 반환
- roles(): 역할 부여 - 합치기도 가능