@DisplayName("프로필 테스트")
public class ProfileServiceTest {
private UserRepository userRepository;
private ProfileService profileService;
private PasswordCheckingRepository passwordCheckingRepository;
@BeforeEach
void setUp() {
userRepository = mock(UserRepository.class);
passwordCheckingRepository = mock(PasswordCheckingRepository.class);
profileService = new ProfileService(userRepository, passwordCheckingRepository);
}
@Test
@Transactional
@DisplayName("비밀번호 수정 테스트 성공")
void testUpdateUserPassword(){
// Given
Long userId = 1L;
String username = "testus12";
String email = "test@example.com";
String password = "PassWo12";
String intro = "Test intro";
var mockUser = User.builder()
.username(username)
.password(password)
.email(email)
.role(USER)
.intro(intro)
.build();
PasswordChecking passwordChecking = new PasswordChecking(mockUser.getPassword(),mockUser);
when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));
when(passwordCheckingRepository.findByUser(mockUser)).thenReturn(passwordChecking);
// When
String newPassword1 = "PAssword45";
String newPassword2 = "PAsSword85";
String newPassword3 = "PAsSword81";
String newPassword4 = "PAsSword82";
profileService.updateUserPassword(userId, newPassword1);
profileService.updateUserPassword(userId, newPassword2);
profileService.updateUserPassword(userId, newPassword3);
profileService.updateUserPassword(userId, newPassword4);
// Then
assertThat(mockUser.getPassword()).isEqualTo(newPassword4);
}
}
비밀번호 변경이력 3회 안으로 같은 비밀번호를 입력했을때 변경 실패를 하는 테스트 중 일단 4번의 비밀번호 변경이 다 다른 경우 비밀번호 변경 성공하는 단위 테스트이다.
userRepository = mock(UserRepository.class);: Mockito를 사용하여 UserRepository의 mock 객체를 생성한다. Mock 객체는 실제 구현이 아닌, 가짜 객체로써 테스트에서 사용된다.
@Transactional: 해당 테스트 메소드는 트랜잭션을 사용하며, 실행 후 롤백되어야 함을 나타낸다. 테스트에서 DB 상태 변경을 테스트한 후에 롤백하여 테스트 간 영향을 최소화한다. @Test 어노테이션과 같이쓰일 경우.
when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));: Mockito를 사용하여 userRepository의 findById 메소드가 호출될 때, 지정한 userId에 해당하는 mock 객체 mockUser를 반환하도록 설정한다.
* 로그인 유저의 상태 관리 서비스를 제공합니다.
* 1. JWT 기반 로그인 유저 정보 조회
* 2. 이메일 인증 코드 관리 서비스
*/
@Component
public class UserStatusService {
private final ConcurrentMap<String, String> redis = new ConcurrentHashMap<>();
/**
* JwtAuthorizationFilter에서 인가된 jwt 토큰 정보를 SecurityContext에서 조회한다.
*
* @return jwt 토큰에 담긴 유저 정보 dto를 반환
*/
public JwtUser getLoginUser() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth.getPrincipal() instanceof JwtUser) return (JwtUser) auth.getPrincipal();
throw new AccessDeniedException("권한이 없습니다.");
}
public void saveEmailAuthCode(String email, String code) {
redis.put(email, code);
}
public void removeEmailAuthCode(String email){
redis.remove(email);
}
public boolean matchesEmailAuthCode(String email, String code) {
if (!redis.containsKey(email)) return false;
return redis.get(email).equals(code);
}
}
@Component: 해당 클래스가 Spring 컴포넌트임을 나타내며, 컴포넌트 스캐닝 중 자동으로 감지된다. 이것은 이 클래스를 Spring 빈으로 자동 등록한다.
(전반적인 프로그래밍 맥락에서)컴포넌트 : 각 부분을 개별적으로 개발, 관리, 배포할 수 있는 재사용 가능한 모듈 단위를 가리킨다.
ConcurrentMap은 Java에서 동시성을 지원하는 맵 인터페이스이다. 여러 스레드가 동시에 맵을 읽고 쓸 수 있도록 설계되었다. 기존의 HashMap과는 달리 ConcurrentMap은 동시성 문제를 해결하기 위해 설계되었다. ConcurrentMap 인터페이스는 다수의 구현체를 가지고 있는데 Java에서 가장 널리 사용되는 구현체 중 하나는 ConcurrentHashMap이다.
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class ConcurrentMapExample {
public static void main(String[] args) {
// ConcurrentMap 생성
ConcurrentMap<Integer, String> concurrentMap = new ConcurrentHashMap<>();
// 요소 추가
concurrentMap.put(1, "One");
concurrentMap.put(2, "Two");
concurrentMap.put(3, "Three");
// 요소 가져오기
System.out.println("Key 1: " + concurrentMap.get(1));
System.out.println("Key 2: " + concurrentMap.get(2));
System.out.println("Key 3: " + concurrentMap.get(3));
// 요소 제거
concurrentMap.remove(2);
// 요소가 제거되었는지 확인
System.out.println("Key 2 after removal: " + concurrentMap.get(2));
// 동시에 여러 스레드에서 맵 수정 가능
Runnable runnable = () -> {
for (int i = 0; i < 1000; i++) {
concurrentMap.putIfAbsent(i, "Value" + i);
}
};
// 여러 스레드에서 맵 수정
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
// 모든 작업이 끝날 때까지 기다림
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 맵의 크기 출력
System.out.println("ConcurrentMap size: " + concurrentMap.size());
}
}
Thread 관련 메소드
putIfAbsent() 메서드를 사용하여 여러 스레드가 안전하게 맵을 수정할 수 있다. 이를 통해 동시성 문제를 방지하면서 안전하게 맵을 조작할 수 있다.
thread1.join() , thread2.join()은 현재 실행 중인 스레드(일반적으로 메인 스레드)가 thread1이라는 스레드가 종료될 때까지 기다리도록 하는 메소드이다. 이 메소드는 예외처리를 해주어야 하므로 try-catch 블록 내에서 사용된다.
public JwtUser getLoginUser() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth.getPrincipal() instanceof JwtUser) return (JwtUser) auth.getPrincipal();
throw new AccessDeniedException("권한이 없습니다.");
}
getLoginUser(): Spring Security의 SecurityContextHolder를 사용하여 현재 로그인한 사용자의 정보를 가져온다. 인증 주체를 확인하고 주체가 JwtUser 타입인 경우에만 JwtUser 객체를 반환하며, 그렇지 않으면 AccessDeniedException을 발생시킨다.
SecurityContextHolder.getContext().getAuthentication(): 이 부분은 스프링 시큐리티의 SecurityContextHolder를 통해 현재 사용자의 인증 정보(Authentication)를 가져온다. SecurityContextHolder는 인증과 관련된 정보를 스레드 로컬 변수에 저장하고 제공하는 역할을 한다.
AbstractAuthenticationToken : Spring Security에서 인증 과정을 추상화하고, 실제 인증된 주체와 관련된 정보를 다루며, 이를 토대로 인증된 사용자의 권한 등을 관리하는 데 사용된다. 주로 사용자 인증 및 권한 부여를 위한 기본적인 메서드와 동작을 정의하고 있다.
this.authorities = Collections.unmodifiableList(new ArrayList<>(authorities)); : AbstractAuthenticationToken 클래스에서 권한(authorities)을 불변(immutable)하게 만드는 과정을 수행한다.