get은 단순 조회용, post는 생성 및 업데이트와 같이 db의 값이 변화하는 경우에 사용
API
이메일 인증 후 회원 가입
// 이메일 인증 확인 후 회원가입
@PostMapping("/verify-email")
public ResponseEntity<ApiResponse>verifyEmail(@RequestParam @Valid UUID emailTokenId){
UserTemp userTemp = userService.checkEmailToken(emailTokenId);
User signUpUser = userService.signUpUser(userTemp);
ApiResponse response = new ApiResponse( "회원 가입 완료",signUpUser);
return new ResponseEntity<>(response, HttpStatus.OK);
}
이메일 인증을 받은 다음 회원 가입을 처리해주는 메서드인 verifyEmail은 회원을 생성하는 거니까 get X post방식
스케쥴러로 미인증 회원들 삭제기능 만들었는데 임시회원이 인증 받으면 이메일 토큰 테이블이랑 임시회원 테이블에서 데이터 삭제하고 , 미인증 받은 상태로 일정 시간지나면 두 테이블에 있는 데이터들 전부 삭제한다. 근데 테이블에 있는 데이터 들을 전부 삭제한 상태에서 새로운 임시 회원이 들어오면 id값이 1 부터 시작되는게 아니라 이전 id값 다음값 부터 시작하게 된다. 이런 경우 데이터가 쌓이면 쌓일수록 계속 id값이 계속 커지기만 한다. 이때 db의 성능과 관련한 문제가 생기지 않을까? 계속해서 id값이 커지더라도 상관 없을까?
테이블이 전부 삭제된 후에 새로운 회원이 들어왔는데 이전 회원의 id값 다음값이 생성된다면 그 앞에 있는 id값들이 낭비 되는거 아닐까?
예를 들어서 100명이 임시 회원 가입을 했다가 인증을 받아서 회원가입해서 삭제되어 테이블이 전부 비워진 상태에서 새로운 데이터가 생성되는데 id값이 101부터 시작하면 1~100의 값은 사용되지 못하는 거니까 테이블 낭비 아닐까??
원인 분석
@Table(name = "USERTEMP_TABLE")
public class UserTemp {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="USERTEMP_ID")
private Long userTempId;
}
현재 내 userTemp 테이블의 pk는 자동으로 값이 증가하도록 설정해놨다.
이 설정은 데이터베이스가 자동 증가된 값을 관리하도록 하고, 삭제된 레코드의 ID를 재사용하지 않는다.
그래서 삭제된 후 새로운 레코드를 삽입하면 연속되지 않는 ID를 얻게 되는 것이다.
해결 방법
id값이 계속해서 커지거나 연속적이지 않은 id값이 만들어진다고 해서 성능에 큰 차이가 발생하지는 않는다고 한다.
오히려 연속적인 id 값을 만들기 위해 아래와 같은 설정을 했다가 트랜잭션과 데이터 무결성 부분에서 문제가 발생할 수 있다
굳이 연속적으로 id값을 생성해야하는 이유가 아니라면 다른 설정을 해줄 필요는 없을 것 같다.
The -m (or --create-home) option tells useradd to create the user’s home directory if it doesn’t already exist. This option ensures that the specified home directory is created and that the default configuration files from /etc/skel are copied into it.
-d Option
The -d (or --home) option specifies the path to the home directory for the new user. However, by itself, -d does not create the home directory. It merely sets the path for the home directory that should be created.
Combined Usage
When you use both -m and -d together, useradd will create the home directory specified by the -d option. If you only use the -d option without -m, the home directory will not be created automatically.
Summary of Differences
-m: Creates the home directory and copies default files from /etc/skel.
-d: Specifies the path for the home directory but does not create it unless used with -m.
사용자에게 이메일 인증을 보내면 이메일 토큰을 생성하고 사용자가 해당 인증 링크를 클릭하면 이메일 토큰을 검증한다.
검증하는 것은 두가지로 1. 해당 uuid 값이 존재하는 값인지 2. 해당 이메일 토큰이 만료된 토큰인지 를 검사한다
이메일 토큰을 생성할때 생성시간+5분 으로 만료시간을 설정해서 값을 저장해둔다.
이후에 사용자가 전달해온 uuid로 이메일 토큰을 찾고, 해당 이메일 토큰에 저장되어있는 만료시각과 현재 시각을 비교하여 만료 여부를 판단한다.
그래서 만료 되었다면 isEmailExpired = true 로 필드의 값을 수정한다. 이후 오류 메세지를 반환한다
문제 상황
오류 메세지는 잘 반환이 되는데 해당 이메일 토큰의 필드값이 업데이트 되지 않았다. (sql 쿼리가 작성되지 않았다)
그런데 이상한건 원래 설정값이 false라서 true가 아닌 이상 오류 메세지를 출력할 수 없는데 오류 메세지는 잘 출력 되었다.
원인 파악
일단 오류 메세지가 나왔다는 것은 필드의 값이 수정은 되었다는 것, 그런데 바뀐값이 db에 반영되지 않았다는 것이다.
아마 문제의 원인은 트랜잭션에 있는 것 같다.
UserService
@Transactional
public class UserService {
@SuppressWarnings("all")
public UserTemp checkEmailToken(UUID emailTokenId){
// 토큰 검증
EmailToken emailToken = emailService.checkEmailToken(emailTokenId);
// 임시 회원의 이메일 인증 완료
Optional<UserTemp> findUserTemp = userTempRepository
.findById(emailToken.getUserTempId());
findUserTemp.ifPresent(UserTemp::emailVerifiedSuccess);
return findUserTemp.get();
}
}
checkEmailToken
url을 통해 받아온 uuid를 이용하여 토큰을 검증하고 검증이 완료되었다면 임시회원의 이메일 인증 완료 처리를 한 후 userTemp 객체를 반환한다.
이때 userService 트랜잭션이 설정되어있기 때문에 checkEmailToken 메서드가 성공적으로 끝나야 그 변화가 db에 저장된다.
EmailService
public class EmailService {
// 유효한 토큰 가져오기
@SuppressWarnings("all")
public EmailToken checkEmailToken(UUID emailTokenId) {
// emailToken 이 존재하는지 검사
EmailToken emailToken = emailTokenRepository.findByEmailTokenId(emailTokenId)
.orElseThrow(() -> new NoSuchElementException("해당 emailTokenId 를 가진 회원이 없습니다"));
// 해당 토큰의 만료 시간 검사
if (!emailToken.isValid()) {
expired(emailToken);
throw new IllegalStateException("해당 이메일은 만료되었습니다. 이메일을 재인증 해주세요");
}
emailToken.usedToken();
return emailToken;
}
@Transactional
public void expired(EmailToken emailToken){
emailToken.setEmailTokenExpired(true);
emailTokenRepository.save(emailToken);
}
}
EmailToken
public class EmailToken {
// 토큰 만료 시간 검증
public boolean isValid() {
return !LocalDateTime.now().isAfter(certificationTime);
}
}
이메일 토큰의 만료 시간을 검증할때 isValid 메서드를 호출하여 만료 시간이 지난뒤 인증 요청이 들어온 경우
expired 메서드를 호출하여 해당 이메일 토큰의 필드값을 수정한 후 오류 메세지를 띄우려고 했다.
expired 메서드에 트랜잭션을 설정했기 때문에 expired 메서드를 통해 생긴 변화( 필드값 변경후 다시 db에 저장) 가 db에 반영될 것이라 생각했다. 그런데 로그를 보면 sql이 작성되지 않았다.
물론 userService의 checkEmailToken 에 트랜잭션이 설정되어있지만 checkEmail에서 호출한 emailService 안의 expired에도 트랜잭션이 설정되어있기 때문에 expired의 트랜잭션이 먼저 끝나서 해당 변화가 db에 저장될 것이라고 생각했다.
GPT 답변
Transaction Committing:
Since UserService.checkEmailToken is a transactional method, the transaction will only be committed after this method completes. Any changes made within this transaction, including changes made in EmailService.expired, will not be committed to the database until the UserService.checkEmailToken method completes successfully.
Transaction Rollback:
If an exception is thrown before the transaction commits, all changes will be rolled back. Since you're throwing an IllegalStateException after marking the token as expired, this might cause the transaction to roll back if not handled properly.
해결 방법
@Transaction 어노테이션을 클래스 범위에서 삭제했다. ( 굳이 클래스단에 붙여줄 이유가 있을까? )
트랜잭션이란 하나의 작업이 성공적으로 완료 될 때 까지 값을 db에 저장 하지 않는것을 말한다. 트랜잭션은 하나의 작업이 성공하면 저장하고, 작업 진행 도중 실패 했다면 진행되었던 작업들을 롤백시킨다
즉, 변화된 값이 있더라도 트랜잭션으로 설정해둔 작업이 모두 끝마칠때 까지 변화된 값을 db에 바로 커밋(저장)하지 않겠다는 것이다.
위의 코드를 보면 이메일 전송을 하는 userService에 트랜잭션이 설정되어있다. 이 뜻은 sendAuthEmail 의 작업이 성공적으로 끝날때 까지 이메일 토큰이 생성되어도 해당 이메일 토큰을 db에 저장하지 않겠다는 것을 의미한다.
따라서 이메일이 성공적으로 보내졌을때 sendAuthEmail의 트랜잭션이 끝나고 이후에 커밋하지 못했던 이메일 토큰의 변화를 db에 저장하는 것이다.
사용자에게 이메일 인증 메세지를 보내지 못했는데 이메일 토큰을 만들어서 미리 저장하는 것이 의미가 있을까? 의미가 없다!! 이메일 토큰을 생성했다는 것 자체가 회원의 이메일 인증을 처리 하려고 하는 것이기 때문에 인증 메일이 보내지지 않은 상황에서는 이메일 토큰이 만들어져봤자 무의미 하다
선택 정렬이란 정렬 해야하는 값들 중에서 가장 작은수를 선택하여 맨 앞으로 보내는 방법이다.
예를 들어 3 2 4 6 8 1 5 가 있다고 하자. 그렇다면 여기서 가장 작은 수는 1 이므로 1을 맨 앞으로 보낸다.
가장 앞에 있는 1은 정렬이 되었으므로 이제 두번째 자리부터 가장 작은 수를 찾는다.
3 2 4 6 8 1 5
1 2 4 6 8 35
1 2 4 6 8 35
다음으로 가장 작은 수는 2이다. 가장 작은 수인 2가 맨 앞에 있으므로 수열값은 변화하지 않는다.
이제 2번째 자리 까지 가장 작은 수를 찾았으므로 3번째 자리 부터 끝까지 가장 작은 수를 찾아 세번째 자리에 넣어준다.
이렇게 한번씩 반복할때 마다 가장 작은 수를 가장 앞에 위치 하게 하는 방식이 선택 정렬이다.
처음 반복문을 수행할때는 첫번째 자리 부터 끝까지 돌고, 그중 가장 작은 수를 찾아 첫번째 자리와 바꾼다. 두번째 반복문을 수행할때는 두번째 자리부터 끝까지 돌고, 그중 가장 작은 수를 찾아 두번째 자리와 바꾼다. 세번째 반복문을 수행할때는 세번째 자리부터 끝까지 돌고, 그중 가장 작은 수를 찾아 세번째 자리와 바꾼다.
이런식으로 계속 반복하여 모든 수를 정렬한다.
#include <iostream>
using namespace std;
int main(){
int idx; // 최솟값을 가진 원소의 인덱스
int arr[10]={2,6,3,8,4,5,9,1,10,7};
for(int i=0;i<9;i++){
int min=999;
for(int j=i;j<10;j++){
// 최솟값 찾기
if(min>arr[j]){
min=arr[j];
idx=j;
}
}
// 최솟값을 맨 앞의 원소의 값과 바꾸기
int temp=arr[i];
arr[i]=arr[idx];
arr[idx]=temp;
}
for(int i=0;i<10;i++){
cout<<arr[i]<<" ";
}
}
선택 정렬의 시간 복잡도 : o(N^2)
선택 정렬로 N개의 수를 정렬하기 위해서는 몇 번의 반복문이 수행되어야 할까?
선택 정렬은 한번 반복문이 수행 될때 마다 한개씩 정렬이 일어나며, 그 값을 제외하고 맨 마지막 값 까지 반복문을 수행한다.
즉, n+(n-1)+(n-2)+ ... +2+1 = n(n-1)/2 번의 수행 시간이 필요하다. (이때 n^2에 비해 상수값들은 무의미 하므로 무시한다)
예를 들어 10개의 수를 정렬하기 위해서는 대략 100번의 계산을 하고, 10,000개라면 약 1억번의 계산을 해야하는 셈이다.
지수함수의 특성 처럼 n의 값(x값)이 증가할때 계산값인 y값의 증가 속도는 기하급수적이다.
그렇다면 과연 이런 정렬 방법이 효율적이라고 할 수 있을지 고민해보자.
버블 정렬
#include <stdio.h>
int length=10;
int data[10]={1,4,2,3,5,10,9,8,6,7};
void bubble_sort(int *data,int size){
for(int i=0;i<size-1;i++){
for(int j=0;j<size-1-i;j++){
if(data[j]>data[j+1]){
int temp=data[j];
data[j]=data[j+1];
data[j+1]=temp;
}
}
}
}
void show(){
for(int i=0;i<length;i++){
printf("%d ",data[i]);
}
}
int main() {
bubble_sort(data,length);
show();
return 0;
}
삽입 정렬
#include <iostream>
using namespace std;
int main() {
int arr[10]={1,10,5,8,7,6,4,3,2,9};
for(int i=1;i<10;i++){
for(int j=i-1;j>=0;j--){
if(arr[j]>arr[j+1]) {
int temp=arr[j+1];
arr[j+1]=arr[j];
arr[j]=temp;
}
else break;
}
}
for(int i=0;i<10;i++){
cout<<arr[i]<<' ';
}
return 0;
}
퀵 정렬
피벗값을 이용하여 큰 값과 작은값을 반복적으로 바꿔준다.
분할 정복
#include <stdio.h>
int number=10;
int data[10]={1,10,5,8,7,6,4,3,2,9};
void quickSort(int *data,int start,int end){
if(start>=end){ // 원소가 1개인 경우 그대로 두기
return ;
}
int key=start; // 키는 첫번째 원소
int i=start+1;
int j=end;
int temp;
while(i<=j){ // 엇갈릴때 까지 반복
while(i<=end && data[i]<=data[key]){ // 키 값보다 큰 값을 만날때 까지
i++;
}
while(j>start && data[j]>=data[key] ){ // 키 값보다 작은 값을 만날때 까지
j--;
}
if(i>j){ // 엇갈린 경우 키 값과 교체
temp=data[j];
data[j]=data[key];
data[key]=temp;
}
else{ // 엇갈리지 않았다면 i와 j를 교체
temp=data[i];
data[i]=data[j];
data[j]=temp;
}
}
quickSort(data,start,j-1);
quickSort(data,j+1,end);
}
void show(){
for(int i=0;i<number;i++){
printf("%d ",data[i]);
}
}
int main() {
quickSort(data,0,number-1);
show();
return 0;
}
병합 정렬
#include <iostream>
using namespace std;
const int number = 8;
int sorted[number];
void merge(int arr[], int m, int middle, int n) {
int i = m;
int j = middle + 1;
int k = m;
while (i <= middle && j <= n) {
if (arr[i] <= arr[j]) {
sorted[k] = arr[i];
i++;
} else {
sorted[k] = arr[j];
j++;
}
k++;
}
while (i <= middle) {
sorted[k] = arr[i];
i++;
k++;
}
while (j <= n) {
sorted[k] = arr[j];
j++;
k++;
}
for (int t = m; t <= n; t++) {
arr[t] = sorted[t];
}
}
void mergeSort(int arr[], int m, int n) {
if (m < n) {
int middle = (m + n) / 2;
mergeSort(arr, m, middle);
mergeSort(arr, middle + 1, n);
merge(arr, m, middle, n);
}
}
int main() {
int array[number] = {2,4,3,6,5,1,7,8};
mergeSort(array, 0, number - 1);
for (int i = 0; i < number; i++) {
cout << array[i] << " ";
}
return 0;
}
uuid 필드를 string -> UUID로 변경하는 과정에서 emailTokenRepository의 메서드 중 파라미터 값을 수정하지 않아서 테이블을 만드는 과정에서 타입이 불일치 돼서 발생한 문제
저 오류의 경우 데이터베이스와 코드의 필드가 맞지 않는 문제 일 수 있음
로그를 잘 읽어보면 문제가 보임
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'emailService' defined in file [C:\Dongurami_jh\USW-Circle-Link-Server\build\classes\java\main\com\USWCicrcleLink\server\email\service\EmailService.class]: Unsatisfied dependency expressed through constructor parameter 2: Error creating bean with name 'emailTokenRepository' defined in com.USWCicrcleLink.server.email.repository.EmailTokenRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Could not create query for public abstract java.util.Optional com.USWCicrcleLink.server.email.repository.EmailTokenRepository.findByEmailTokenIdAndCertificationTimeAfterAndIsEmailTokenExpired(java.lang.String,java.time.LocalDateTime,boolean); Reason: Failed to create query for method public abstract java.util.Optional com.USWCicrcleLink.server.email.repository.EmailTokenRepository.findByEmailTokenIdAndCertificationTimeAfterAndIsEmailTokenExpired(java.lang.String,java.time.LocalDateTime,boolean); Cannot compare left expression of type 'java.util.UUID' with right expression of type 'java.lang.String'