728x90
Environment
Language: Java17
Framework: SpringBoot 3.1.0

 

오류

constructor Jackson2JsonRedisSerializer.Jackson2JsonRedisSerializer(Class<T>) is not applicable(argument mismatch; Class<CAP#2> cannot be converted to Class<T>)
constructor Jackson2JsonRedisSerializer.Jackson2JsonRedisSerializer(JavaType) is not applicable(argument mismatch; Class<CAP#2> cannot be converted to JavaType)
where T is a type-variable:
  T extends Object declared in method <T>set(String,T,long)
where CAP#1,CAP#2 are fresh type-variables:
  CAP#1 extends Object from capture of ? extends Object
  CAP#2 extends Object from capture of ? extends Object

Jackson2JsonRedisSerializer의 생성자에서 Class<T> 타입을 요구

컴파일러가 제네릭 타입을 캡처한 결과인 CAP#2는 T 타입으로 변환할 수 없음

  * CAP#1, CAP#2: 컴파일러가 제네릭 타입을 추론할 때 사용하는 임시 이름으로, T가 ? extends Object (모든 타입을 포함하는 와일드카드)로 사용될 때 부여

 

 

 

원인

public <T> void set(String key, T o, long milliseconds) {
  redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<T>(o.getClass()));
  redisTemplate.opsForValue().set(key, o, milliseconds, TimeUnit.MILLISECONDS);
}

제너릭 타입(<>)에 파라미터(T)를 명시할 경우, 컴파일러가 T의 타입을 자동으로 추론할 수 없음

T의 명확한 타입을 아는 것이 아니라 단순히 와일드 카드(? extends Object)인 것만 알고 있는 상태이므로 o.getClass와 T를 매칭시키지 못함

따라서, 타입을 명시했지만 컴파일 시점에는 타입을 알 수 없어 오류 발생

 

 

해결

public <T> void set(String key, T o, long milliseconds) {
  redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(o.getClass()));
  redisTemplate.opsForValue().set(key, o, milliseconds, TimeUnit.MILLISECONDS);
}

파라미터를 명시하지 않고 타입만 선언(<>)하여, 컴파일러에게 타입 추론 자체를 위임

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

Chat GPT

 

728x90
728x90

java.lang.NullPointerException: Cannot invoke "org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.encode(java.lang.CharSequence)" because "this.bCryptPasswordEncoder" is null

Environment
Language: Java 17
Framework: SpringBoot 3.1.0, Spring Security 6.1.0
DB: MySQL, Redis

 

오류

java.lang.NullPointerException: 
Cannot invoke "org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.encode(java.lang.CharSequence)" 
because "this.bCryptPasswordEncoder" is null

 

 

원인

@Builder
@AllArgsConstructor
@Getter
public class SignUpRequestDTO {

  private final BCryptPasswordEncoder bCryptPasswordEncoder;

  public User toEntity() {
    return User.builder()
               .status(userType.substring(0, 1).toUpperCase())
               .userType(UserType.valueOf(userType.toUpperCase()))
               .email(email)
               .phone(phone)
               .nickname(nickname)
               .password(bCryptPasswordEncoder.encode(password))
               .role(getRole())
               .build();
  }
}

bCryptPasswordEncoder 인스턴스가 주입되지 않음

 

SignUpRequestDTO가 빈으로 등록되지 않았으므로 Spring에서는 @Bean으로 등록된 BCryptPasswordEncoder를 자동으로 주입할 수 없음

 

해결

1. SignUpRequestDTO을 Bean으로 등록

🚨 DTO는 일반적으로 데이터 전송 객체로 사용되며, 빈으로 관리되는 경우가 거의 없음

@Bean
@Builder
@AllArgsConstructor
@Getter
public class SignUpRequestDTO {

  private final BCryptPasswordEncoder bCryptPasswordEncoder;

  public User toEntity() {
    return User.builder()
               .status(userType.substring(0, 1).toUpperCase())
               .userType(UserType.valueOf(userType.toUpperCase()))
               .email(email)
               .phone(phone)
               .nickname(nickname)
               .password(bCryptPasswordEncoder.encode(password))
               .role(getRole())
               .build();
  }
}

 

2. Service 클래스에서 BCryptPasswordEncoder를 주입

UserService.java
Bean으로 등록된 UserSerivce에서 BCryptPasswordEncoder를 주입받아 SignUpRequestDTO로 전달
@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserService {

  private final BCryptPasswordEncoder bCryptPasswordEncoder;

  @Transactional
  public void create(SignUpRequestDTO signUpRequestDTO) {
    emailDuplicateCheck(signUpRequestDTO.getEmail());
    userRepository.save(signUpRequestDTO.toEntity(bCryptPasswordEncoder));

    log.info("User successfully created for email: {}", signUpRequestDTO.getEmail());
  }
}

 

SignUpRequestDTO.java
@Builder
@AllArgsConstructor
@Getter
public class SignUpRequestDTO {

  public User toEntity(BCryptPasswordEncoder bCryptPasswordEncoder) {
    return User.builder()
               .status(userType.substring(0, 1).toUpperCase())
               .userType(UserType.valueOf(userType.toUpperCase()))
               .email(email)
               .phone(phone)
               .nickname(nickname)
               .password(bCryptPasswordEncoder.encode(password))
               .role(getRole())
               .build();
  }
}

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

 

728x90
728x90

Factory method 'dataSource' threw exception with message: Failed to determine a suitable driver class

 근데 이제 DataSource와 Drive class를 곁들인

Environment
Language: Java 17
Framework: SpringBoot 3.1.0, Junit5
DB: MySQL, Redis

 

오류

dataSourceScriptDatabaseInitializer의 Bean 생성을 실패

적합한 드라이버 클래스를 찾지 못함

 

 

원인

.gitignore에 환경변수 설정과 관련된 정보가 들어있는 application.properties를 추가

github에서 application.properties가 제거된 채로 업로드

왜인지 모르겠지만.. 로컬에서도 함께 사라짐..

 

 

해결

application.properties 파일 복구

 

 

DataSource와 DriverClass

자바 언어로 DB에 접근하기 위해 사용하는 API를 JDBC라고 합니다.

JDBC를 사용해서 Oracle Database, MySQL 등 다양한 DB에 일관된 방법으로 접근할 수 있습니다.

JDBC API를 사용하기 위해서는 JDBC 드라이버를 먼저 로딩한 후 데이터베이스와 연결해야 합니다.

  * JDBC 드라이버: JDBC 인터페이스를 구현한 구현체입니다.

 

DB와 통신하는 과정은 다음과 같습니다.

1. DB Driver를 Load

2. DriverManager를 통해 Connection을 획득

  * DriverManager: 드라이버들을 관리하고 Connection을 획득하는 기능을 제공

3. 질의 수행

4. 결과 획득

5. Connection 종료

 

문제는 이렇게 매번 커넥션을 생성하기 위해서는 네트워크와 연결하고 서버의 자원을 사용한다는 점입니다.

즉, 연결하는 데에 추가적인 시간이 걸리며 매번 리소스를 사용하게 된다는 것입니다.

이러한 문제점을 해결하기 위해 커넥션 풀(Connection Pool)을 사용합니다.

즉, 데이터베이스와의 연결이 필요할 때마다 매번 새로운 커넥션을 생성하는 대신 미리 생성된 커넥션을 커넥션풀에 보관하고 필요할 때마다 커넥션을 꺼내서 사용하는 것입니다.

 

이처럼 커넥션을 획득할 때 DriverManager를 통해서 커넥션을 획득하거나 커넥션풀을 통해서 커넥션을 획득하는 등 여러 방법이 존재합니다.

그래서 DataSource 인터페이스를 통해서 커넥션을 획득하는 방법을 추상화합니다.

이를 통해 커넥션을 획득하는 방식과는 무관하게 일관된 방식으로 데이터베이스와 통신할 수 있는 것이다.

 

이 때, 오류가 HikariDataSource인 것을 보아서 미리 커넥션을 생성해 놓은 커넥션풀을 사용하고 있다는 걸 알 수 있습니다.

  + DriverManagerDataSource의 경우, DB에 연결할 때마다 커넥션을 생성해서 획득하는 방식입니다.

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

https://tecoble.techcourse.co.kr/post/2023-06-28-JDBC-DataSource/

 

JDBC와 DataSource 이해하기

JDBC란? JDBC(Java Database Connectivity)는 자바 프로그래밍 언어를 사용해 데이터베이스에 접근할 수 있도록 하는 자바 API이다. 이를 통해서 우리는 데이터베이스에 접속하고, SQL…

tecoble.techcourse.co.kr

 

 

728x90
728x90

org.mockito.exceptions.verification.TooManyActualInvocations

Environment
Language: Java 17
Framework: SpringBoot 3.1.0, Junit5
DB: MySQL, Redis

 

 

오류

org.mockito.exceptions.verification.TooManyActualInvocations: 
userService bean.create(
    refEq(cohttp://m.msgs.domain.user.dto.SignUpRequestDTO@69bf351e)
);
Wanted 1 time:
-> at cohttp://m.msgs.domain.user.service.UserService.create(UserService.java:41)
But was 2 times:
-> at cohttp://m.msgs.domain.user.controller.UserControllerTest.setup(UserControllerTest.java:45)
-> at cohttp://m.msgs.domain.user.controller.UserController.create(UserController.java:30)

UserService의 create 메서드가 너무 많이 호출됨

1번을 의도했으나, 2번이 호출됨

 

 

원인

@SpringBootTest
@AutoConfigureMockMvc(addFilters = false)
class UserControllerTest {
    @BeforeEach
    void setup() {
        SignUpRequestDTO signUpDto = SignUpRequestDTO.builder()
                .status("M")
                .email("temp@email.com")
                .phone("01023698745")
                .nickname("name")
                .password("temp123!")
                .confirmPassword("temp123!")
                .build();

        userService.create(signUpDto);
    }

    @Test
    @DisplayName("Controller: 회원 가입 실패, 중복된 이메일")
    void createFailDuplicateEmail() throws Exception {
        // given
        SignUpRequestDTO signUpDto = SignUpRequestDTO.builder()
                .status("M")
                .email("temp@email.com")
                .phone("01023698745")
                .nickname("name")
                .password("temp123!")
                .confirmPassword("temp123!")
                .build();

        // when // then
        mockMvc.perform(post("/api/v2/users/new")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(signUpDto)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.errorMessage").value("이미 존재하는 이메일 입니다."));

        // 회원 생성 메소드가 호출되었는지 확인
        verify(userService).create(refEq(signUpDto));
    }
}

중복 회원 검사를 위해 Test전 @BeforeEach를 사용해서 UserService의 create 메서드를 호출

+ 중복회원 검증 시, UserService의 create 메서드를 호출

→ 총 create 메서드 2회 호출

→  verify 메서드에서 예상 호출 수와 실제 호출 수의 불일치가 발생하여 TooManyActualInvocations 예외 발생

 

 

해결

@SpringBootTest
@AutoConfigureMockMvc(addFilters = false)
class UserControllerTest {
    @Test
    @DisplayName("Controller: 회원 가입 실패, 중복된 이메일")
    void createFailDuplicateEmail() throws Exception {
        // given
        SignUpRequestDTO signUpDto = SignUpRequestDTO.builder()
                .status("M")
                .email("temp@email.com")
                .phone("01023698745")
                .nickname("name")
                .password("temp123!")
                .confirmPassword("temp123!")
                .build();

        // Mocking 중복된 이메일로 인한 예외 발생
        doThrow(new BusinessException(ErrorCode.DUPLICATED_EMAIL))
                .when(userService).create(any(SignUpRequestDTO.class));

        // when // then
        mockMvc.perform(post("/api/v2/users/new")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(signUpDto)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.errorMessage").value("이미 존재하는 이메일 입니다."));

        // 회원 생성 메소드가 호출되었는지 확인
        verify(userService).create(refEq(signUpDto));
    }
}

@BeforeEach를 제거하고 doThrow() 활용

* userService의 create 메서드로 SignUpRequestDTO가 넘겨지면 DUPLICATED_EMAIL Exception 호출로 모킹

 

 

 

📑

참고 자료

 

728x90
728x90

Argument(s) are different!
Actual invocations have different arguments:

Environment
Language: Java 17
Framework: SpringBoot 3.1.0
DB: MySQL, Redis

 

오류

Argument(s) are different! Wanted:
userService bean.create(
    com.msgs.domain.user.dto.SignUpRequestDTO@4dbf902
);
-> at com.msgs.domain.user.service.UserService.create(UserService.java:47)
Actual invocations have different arguments:
userService bean.create(
    com.msgs.domain.user.dto.SignUpRequestDTO@7739bcca
);
-> at com.msgs.domain.user.controller.UserController.create(UserController.java:31)

userService.create()에 인자로 전달한 SignUpRequestDTO의 주소값이 상이

 

원인

@Test
@DisplayName("Controller: 회원 가입")
void create() throws Exception {
    // given
    SignUpRequestDTO signUpDto = SignUpRequestDTO.builder()
            .status("M")
            .email("Tcontroller@email.com")
            .phone("01023698741")
            .nickname("Tname")
            .password("test123!")
            .confirmPassword("test123!")
            .build();

    // when // then
    mockMvc.perform(post("/api/v2/users/new")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(signUpDto)))
            .andExpect(status().isOk());  // 응답이 200 OK 인지 확인

    // 회원 생성 메소드가 호출되었는지 확인
    verify(userService).create(signUpDto);
}

 

⭐ verify(userService).create(signUpDto)는 기본적으로 두 객체의 메모리 주소가 다르면 동일하지 않다고 판단

MockMvc를 통해 요청이 직렬화(SignUpRequestDTO → JSON, body안에 데이터를 넣음)와 역직렬화(JSON → SignUpRequest, body안의 데이터를 DTO 타입으로 변경) 과정을 거치면서 새로운 SignUpRequestDTO 객체가 생성되었고, 그로 인해 객체가 동일하지 않다고 판단

 

해결

@Test
@DisplayName("Controller: 회원 가입")
void create() throws Exception {
    // given
    SignUpRequestDTO signUpDto = SignUpRequestDTO.builder()
            .status("M")
            .email("Tcontroller@email.com")
            .phone("01023698741")
            .nickname("Tname")
            .password("test123!")
            .confirmPassword("test123!")
            .build();

    // when // then
    mockMvc.perform(post("/api/v2/users/new")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(signUpDto)))
            .andExpect(status().isOk());  // 응답이 200 OK 인지 확인

    // 회원 생성 메소드가 호출되었는지 확인
    verify(userService).create(refEq(signUpDto));
}

⭐ refEq() 사용

필드의 값을 비교하여 두 객체가 동일한지 확인

 

 

 

📑

참고 자료

https://zorba91.tistory.com/235

 

[JUnit] Argument(s) are different! Wanted: 에러 해결

temp2 [JUnit] Argument(s) are different! Wanted: JUnit Controller 테스트에서 validation 중에 발생한 에러!! 에러가 설명하듯이 인자가 다르단다. 어? 근데 콘솔 찍히는거보면 똑같은데 왜 다르다고 뜨지?라는 의

zorba91.tistory.com

 

728x90