문제 상황 

ec2 서버를 실행 시킨 후에  포스트맨을 이용하여 서버에 요청을 보내려고 하였다. 

하지만 포스트맨에서 send를 클릭하니 아래와 같은 화면이 나왔다. 

분명 서버도 실행되고 있고 서버 주소도 올바른데 왜 이런 오류가 날까... king 받쥬

 

 

 

해결 방법

https://jemmaa.tistory.com/9

 

POSTMAN error: connect ETIMEDOUT 해결

AWS EC2 IP를 이용해서 POSTMAN에 send를 했는데 error: connect ETIMEDOUT 에러가 떴다. 첫번째로 확인해야할 것은 AWS EC2에서 인바운드 규칙에서 해당 port를 열었는지 확인해야할 것이다. 나는 port를 5001을 썼

jemmaa.tistory.com

 

8080 포트를 열어놓지 않아서 요청이 가지 않았던것 이었다

 

 

햅삐 ~ 

 

문제상황 

 

UserController

@GetMapping("/check-account-duplicate")
    public ResponseEntity<ApiResponse>checkAccountDuplicate(@RequestParam @Valid  String account) throws IllegalAccessException {
        userService.checkAccountDuplicate(account);
        return ResponseEntity.ok(new ApiResponse("사용 가능한 계정입니다."));
    }

 

UserService

 public void checkAccountDuplicate(String account) {
        Boolean isExist = userRepository.findByUserAccount(account);
        if(isExist){
            throw new IllegalAccessException("중복된 ID 입니다. 새로운 ID를 입력해주세요");
        }
    }

 

 

UserRepository

public interface UserRepository extends JpaRepository <User,Long> {
    User findByUserUUID(UUID uuid);
    Boolean findByUserAccount(String account);

}

 

postman 

 

 

아이디 중복 검사 로직

  • 아이디를 param으로 받아서 userRepository(회원 데이터 저장소)에서 해당 아이디가 존재하는지 확인한다. 
  • 아이디가 존재하는 경우 중복된 아이디  처리를 해준다. 

 

포스트맨 요청 방법

  • @requestParam 으로 요청시에는 url로 전달된 값을 가져오기 때문에 주소 입력란에 직접 작성한다. 
  •  또는 밑에 Params 칸에서 key-value 로 직접 값을 설정해주면 된다. 

 

 

 


 

 

원인 분석 

 

The mismatch is happening because the method findByUserAccount in UserRepository is incorrectly defined to return a Boolean when it should actually be returning a User or checking for the existence in a boolean method.

Method Definition Issue
: The method findByUserAccount is expected to return a Boolean, but it is actually returning a User.

 

 

 userRepository에서 account가 존재하는지 안하는지를 찾을때 리턴값을 boolean으로 설정했다. 

하지만 메서드의 이름이 findByUserAccount 였기 때문에 jpa 메서드 이름 규칙에 따라 (boolean값을 return 하는 것이 아닌)

user을 리턴했던것이다.  그래서 userRepositroy 에서 타입이 불일치 한다는 오류가 발생했던것이다. 

 

내가 원하는대로 boolean값을 리턴하도록 하고싶다면 existsBy ~  를 사용했어야한다. 

 


해결방법

public interface UserRepository extends JpaRepository <User,Long> {
    User findByUserUUID(UUID uuid);
    Boolean existsByUserAccount(String account); 
}

 

Boolean의 리턴 값을 갖도록 메서드의 이름을 수정해줬다. 이후 테스트를 진행하니 잘 수행되었다~ 

 

 

※ 참고

 

https://wisdom-cs.tistory.com/66

 

[Spring Data JPA] 기본 사용법 정리

전에 공부했던 Spring Data JPA의 기본 사용법을 정리하고자 한다. ✔️ Dependency build.gradle 파일의 dependencies 부분에 다음을 추가하자. implementation ‘org.springframework.boot:spring-boot-starter-data-jpa’ ✔️ 공

wisdom-cs.tistory.com

 

 

 


                                                         JPA 리포지토리 메서드 이름 명명시 주의하기 

 

JPA 리포지토리에서 메서드를 작성할때는 리턴 타입이 무엇인지에 따라 메서드 명이 바뀌기 때문에 이에 유의해서 메서드 이름을 작성하자 ( 내 맘대로 이름 작성하는거 아님!! ) 

 

문제상황

스케쥴러로 미인증 회원들 삭제기능 만들었는데  임시회원이 인증 받으면 이메일 토큰 테이블이랑 임시회원 테이블에서 데이터 삭제하고 , 미인증 받은 상태로 일정 시간지나면 두 테이블에 있는 데이터들 전부 삭제한다.  근데  테이블에 있는 데이터 들을 전부 삭제한 상태에서 새로운 임시 회원이 들어오면 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값을 생성해야하는 이유가 아니라면 다른 설정을 해줄 필요는 없을 것 같다. 

 

TRUNCATE TABLE user_temp RESTART IDENTITY;
ALTER SEQUENCE user_temp_seq RESTART WITH 1;

 

참고자료 

 

https://transferhwang.tistory.com/112

 

[Spring Boot] h2 Database 인덱스 auto_increment 초기화

스프링 부트 공부를 하면서 h2 DB를 이용해 실습하던 도중 다음과 같은 문제를 만났습니다. 임의의 DB의 테이블에 인덱스 값이 1,2,3,4 인 4개의 레코드가 있다고 가정했을때, 4번 인덱스의 레코드를

transferhwang.tistory.com

 

https://stackoverflow.com/questions/10065386/resetting-autoincrement-in-h2

 

Resetting autoincrement in h2

I'm testing a controller that returns a json response but the tests fail after the first time because the h2 database does not reset the auto increment id. Using fixtures or creating objects manual...

stackoverflow.com

 

 

 

 

사용자에게 이메일 인증을 보내면 이메일 토큰을 생성하고  사용자가 해당 인증 링크를 클릭하면 이메일 토큰을 검증한다. 

검증하는 것은 두가지로 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 에 저장하고 나서 이메일 링크를 만들고 전송하는 기능으로 코드를 짰는데 

실제 동작 결과 이메일 링크를 전송하고 나서, 이메일 토큰을 저장한다. 

 

 

왜 이런 모순이 생겼을까?  그 이유는 @Transaction에 있다. 

 

 

 @Transactional
 public class UserService {
 
 // 이메일 전송
    public UUID sendAuthEmail(UserTemp userTemp) throws MessagingException {
        // 이메일 토큰 생성
        EmailToken emailToken = emailService.createmailToken(userTemp);
        // 이메일 전송
        MimeMessage message = emailService.createAuthLink(userTemp, emailToken);
        emailService.sendEmail(message);

        return emailToken.getEmailTokenId();
    }
    
    }

 

 @Transactional
    public  EmailToken createmailToken(UserTemp userTemp) {
        EmailToken emailToken = EmailToken.createEmailToken(userTemp);
        return emailTokenRepository.save(emailToken);
    }

 

 

트랜잭션이란 하나의 작업이 성공적으로 완료 될 때 까지 값을 db에 저장 하지 않는것을 말한다. 트랜잭션은 하나의 작업이 성공하면 저장하고, 작업 진행 도중 실패 했다면 진행되었던 작업들을 롤백시킨다 

즉, 변화된 값이 있더라도 트랜잭션으로 설정해둔 작업이 모두 끝마칠때 까지 변화된 값을 db에 바로 커밋(저장)하지 않겠다는 것이다. 

 

위의 코드를 보면 이메일 전송을 하는 userService에 트랜잭션이 설정되어있다. 이 뜻은 sendAuthEmail 의 작업이 성공적으로 끝날때 까지 이메일 토큰이 생성되어도 해당 이메일 토큰을 db에 저장하지 않겠다는 것을 의미한다. 

 

따라서 이메일이 성공적으로 보내졌을때 sendAuthEmail의 트랜잭션이 끝나고 이후에 커밋하지 못했던 이메일 토큰의 변화를 db에 저장하는 것이다. 

 

사용자에게 이메일 인증 메세지를 보내지 못했는데 이메일 토큰을 만들어서 미리 저장하는 것이 의미가 있을까? 
의미가 없다!!  이메일 토큰을 생성했다는 것 자체가 회원의 이메일 인증을 처리 하려고 하는 것이기 때문에 인증 메일이 보내지지 않은 상황에서는 이메일 토큰이 만들어져봤자 무의미 하다 

 

 

결론: 이메일이 전송되고 나서 이메일 토큰이 저장되도록 하는 것이 맞다 

 

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'

 

@GenericGenerator 

 

 

문제점 

 'org.hibernate.annotations.GenericGenerator' is deprecated since version 6.5 

 

 

 

해결 방법

https://stackoverflow.com/questions/76723290/using-the-new-type-for-uuidgenerator-instead-of-strategy

 

Using the new type() for UUIDGenerator instead of strategy?

According to the documentation, the following usage is deprecated: @GenericGenerator( name = "UUID", strategy = "org.hibernate.id.UUIDGenerator" ) One

stackoverflow.com

 

 

정리할 것 

  • uuid를 생성하는 방법에는 무엇이 있을까
  • @UuidGenerator란 무엇인가

 

https://www.baeldung.com/java-hibernate-uuid-primary-key

 

 

 

 

+ Recent posts