1번 문제 해결에 대해 확인하니, INSERT 쿼리가 실행되지 않는 문제가 발생함.
-> @Transactional(readOnly = true)로 설정해둬서 모든 메서드가 읽기 전용으로 되는 것이 문제였음.
즉,
@Transactional // 메서드에 트랜잭션을 활성화하여 INSERT 실행 가능
public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequest) {
package org.example.expert.domain.todo.service;
import org.example.expert.client.WeatherClient;
import org.example.expert.domain.common.dto.AuthUser;
import org.example.expert.domain.todo.dto.request.TodoSaveRequest;
import org.example.expert.domain.todo.dto.response.TodoSaveResponse;
import org.example.expert.domain.todo.entity.Todo;
import org.example.expert.domain.todo.repository.TodoRepository;
import org.example.expert.domain.user.dto.response.UserResponse;
import org.example.expert.domain.user.entity.User;
import org.example.expert.domain.user.enums.UserRole;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/*
현재 기준에서, @SpringBootTest + @DataJpaTest + Testcontainers
방식이 가장 많이 쓰이는 것 같아서 해당 방식으로 line 별로 테스트 코드 구현.
*/
@SpringBootTest
@Transactional
public class TodoServiceTest {
private TodoService todoService;
@MockitoBean
private TodoRepository todoRepository;
@MockitoBean
private WeatherClient weatherClient;
@BeforeEach
void setUp() {
todoService = new TodoService(todoRepository, weatherClient);
}
@Test
@DisplayName("할 일 저장 시 User 반환, 날씨 정보, todo 새애성, db 저장을 정상적으로 수행해야 합니다.")
void saveTodo_성공() {
//given
AuthUser authUser = new AuthUser(1L, "test@user.com", UserRole.USER);
TodoSaveRequest request = new TodoSaveRequest("할 일 title", "할 일 내용");
String expectedWeather = "맑음";
when(weatherClient.getTodayWeather()).thenReturn(expectedWeather);
when(todoRepository.save(any(Todo.class))).thenAnswer(
invocation -> {
Todo todo = invocation.getArgument(0);
return new Todo(todo.getTitle(), todo.getContents(), todo.getWeather(), todo.getUser());
}
);
//when
TodoSaveResponse response = todoService.saveTodo(authUser, request);
//then
//AuthUser → User 변환 확인
User expectedUser = User.fromAuthUser(authUser);
assertThat(response.getUser()).isEqualTo(new UserResponse(expectedUser.getId(), expectedUser.getEmail()));
//날씨 정보 가져오는거 확인
verify(weatherClient, times(1)).getTodayWeather();
//투두 생성 확인
ArgumentCaptor<Todo> todoCaptor = ArgumentCaptor.forClass(Todo.class);
verify(todoRepository, times(1)).save(todoCaptor.capture());
Todo savedTodo = todoCaptor.getValue();
assertThat(savedTodo.getTitle()).isEqualTo(request.getTitle());
assertThat(savedTodo.getContents()).isEqualTo(request.getContents());
assertThat(savedTodo.getWeather()).isEqualTo(expectedWeather);
assertThat(savedTodo.getUser()).isEqualTo(expectedUser);
//응답 객체 검증
assertThat(response.getId()).isNotNull();
assertThat(response.getTitle()).isEqualTo(request.getTitle());
assertThat(response.getContents()).isEqualTo(request.getContents());
assertThat(response.getWeather()).isEqualTo(expectedWeather);
}
}
이렇게 테스트 코드를 짜봣는데,
> Task :compileJava UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE
> Task :compileTestJava UP-TO-DATE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test
---- IntelliJ IDEA coverage runner ----
sampling ...
include patterns:
exclude patterns:
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
> Task :test
19:06:56.178 [Test worker] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [org.example.expert.domain.todo.service.TodoServiceTest]: TodoServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
[2025.03.12 19:06:56] (Coverage): Error during class instrumentation: org.springframework.http.client.ReactorResourceFactory: java.lang.RuntimeException: java.io.IOException: Class reactor/netty/resources/ConnectionProvider not found
19:06:56.316 [Test worker] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration org.example.expert.ExpertApplication for test class org.example.expert.domain.todo.service.TodoServiceTest
[2025.03.12 19:06:56] (Coverage): Error during class instrumentation: org.springframework.boot.logging.log4j2.Log4J2LoggingSystem: java.lang.RuntimeException: java.io.IOException: Class org/apache/logging/log4j/core/config/composite/CompositeConfiguration not found
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.4.3)
2025-03-12T19:06:56.679+09:00 INFO 93762 --- [foodduck] [ Test worker] o.e.e.d.todo.service.TodoServiceTest : Starting TodoServiceTest using Java 17.0.14 with PID 93762 (started by mun in /Users/mun/Desktop/2025/spring-plus)
2025-03-12T19:06:56.680+09:00 INFO 93762 --- [foodduck] [ Test worker] o.e.e.d.todo.service.TodoServiceTest : No active profile set, falling back to 1 default profile: "default"
2025-03-12T19:06:57.351+09:00 INFO 93762 --- [foodduck] [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2025-03-12T19:06:57.410+09:00 INFO 93762 --- [foodduck] [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 49 ms. Found 4 JPA repository interfaces.
2025-03-12T19:06:58.859+09:00 INFO 93762 --- [foodduck] [ Test worker] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2025-03-12T19:06:58.949+09:00 INFO 93762 --- [foodduck] [ Test worker] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.6.8.Final
2025-03-12T19:06:59.003+09:00 INFO 93762 --- [foodduck] [ Test worker] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
2025-03-12T19:06:59.219+09:00 INFO 93762 --- [foodduck] [ Test worker] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2025-03-12T19:06:59.272+09:00 INFO 93762 --- [foodduck] [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2025-03-12T19:06:59.647+09:00 INFO 93762 --- [foodduck] [ Test worker] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@5f14590c
2025-03-12T19:06:59.649+09:00 INFO 93762 --- [foodduck] [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2025-03-12T19:06:59.718+09:00 WARN 93762 --- [foodduck] [ Test worker] org.hibernate.orm.deprecation : HHH90000025: MySQLDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default)
2025-03-12T19:06:59.738+09:00 INFO 93762 --- [foodduck] [ Test worker] org.hibernate.orm.connections.pooling : HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.41
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-03-12T19:07:00.676+09:00 INFO 93762 --- [foodduck] [ Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
Hibernate:
alter table comments
drop
foreign key FKhq2jvyd0htxaj4avgceuigt4c
Hibernate:
alter table comments
drop
foreign key FK8omq0tc18jd43bu5tjh6jvraq
Hibernate:
alter table managers
drop
foreign key FKhniowui3ft3l9sdaijwv18id
Hibernate:
alter table managers
drop
foreign key FKsp1db43yf1nqhswrpbwmlnhb9
Hibernate:
alter table todos
drop
foreign key FK9605g76a1dggbvs18f2r80gvu
Hibernate:
drop table if exists comments
Hibernate:
drop table if exists managers
Hibernate:
drop table if exists todos
Hibernate:
drop table if exists users
Hibernate:
create table comments (
created_at datetime(6),
id bigint not null auto_increment,
modified_at datetime(6),
todo_id bigint not null,
user_id bigint not null,
contents varchar(255),
primary key (id)
) engine=InnoDB
Hibernate:
create table managers (
id bigint not null auto_increment,
todo_id bigint not null,
user_id bigint not null,
primary key (id)
) engine=InnoDB
Hibernate:
create table todos (
created_at datetime(6),
id bigint not null auto_increment,
modified_at datetime(6),
user_id bigint not null,
contents varchar(255),
title varchar(255),
weather varchar(255),
primary key (id)
) engine=InnoDB
Hibernate:
create table users (
created_at datetime(6),
id bigint not null auto_increment,
modified_at datetime(6),
email varchar(255),
password varchar(255),
user_role enum ('ADMIN','USER'),
primary key (id)
) engine=InnoDB
Hibernate:
alter table users
add constraint UK6dotkott2kjsp8vw4d0m25fb7 unique (email)
Hibernate:
alter table comments
add constraint FKhq2jvyd0htxaj4avgceuigt4c
foreign key (todo_id)
references todos (id)
Hibernate:
alter table comments
add constraint FK8omq0tc18jd43bu5tjh6jvraq
foreign key (user_id)
references users (id)
Hibernate:
alter table managers
add constraint FKhniowui3ft3l9sdaijwv18id
foreign key (todo_id)
references todos (id)
Hibernate:
alter table managers
add constraint FKsp1db43yf1nqhswrpbwmlnhb9
foreign key (user_id)
references users (id)
Hibernate:
alter table todos
add constraint FK9605g76a1dggbvs18f2r80gvu
foreign key (user_id)
references users (id)
2025-03-12T19:07:00.939+09:00 INFO 93762 --- [foodduck] [ Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2025-03-12T19:07:01.441+09:00 INFO 93762 --- [foodduck] [ Test worker] o.s.d.j.r.query.QueryEnhancerFactory : Hibernate is in classpath; If applicable, HQL parser will be used.
2025-03-12T19:07:02.428+09:00 WARN 93762 --- [foodduck] [ Test worker] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
[2025.03.12 19:07:02] (Coverage): Error during class instrumentation: org.springframework.web.method.support.InvocableHandlerMethod: java.lang.RuntimeException: java.io.IOException: Class reactor/core/publisher/Mono not found
[2025.03.12 19:07:02] (Coverage): Error during class instrumentation: org.springframework.core.ReactiveAdapterRegistry$ReactorAdapter: java.lang.RuntimeException: java.io.IOException: Class reactor/core/publisher/Flux not found
[2025.03.12 19:07:02] (Coverage): Error during class instrumentation: org.springframework.boot.http.client.JettyClientHttpRequestFactoryBuilder: java.lang.RuntimeException: java.io.IOException: Class org/eclipse/jetty/client/transport/HttpClientTransportDynamic not found
[2025.03.12 19:07:02] (Coverage): Error during class instrumentation: org.springframework.boot.http.client.HttpComponentsClientHttpRequestFactoryBuilder: java.lang.RuntimeException: java.io.IOException: Class org/apache/hc/client5/http/protocol/RedirectStrategy not found
2025-03-12T19:07:02.810+09:00 INFO 93762 --- [foodduck] [ Test worker] o.e.e.d.todo.service.TodoServiceTest : Started TodoServiceTest in 6.371 seconds (process running for 7.396)
expected: org.example.expert.domain.user.dto.response.UserResponse@4ce6f39e
but was: org.example.expert.domain.user.dto.response.UserResponse@266e0341
org.opentest4j.AssertionFailedError:
expected: org.example.expert.domain.user.dto.response.UserResponse@4ce6f39e
but was: org.example.expert.domain.user.dto.response.UserResponse@266e0341
at app//org.example.expert.domain.todo.service.TodoServiceTest.saveTodo_성공(TodoServiceTest.java:68)
at java.base@17.0.14/java.lang.reflect.Method.invoke(Method.java:569)
at java.base@17.0.14/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base@17.0.14/java.util.ArrayList.forEach(ArrayList.java:1511)
Class transformation time: 2.561847593s for 14813 classes or 1.72945898400054E-4s per class
2025-03-12T19:07:03.015+09:00 INFO 93762 --- [foodduck] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2025-03-12T19:07:03.016+09:00 INFO 93762 --- [foodduck] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2025-03-12T19:07:03.029+09:00 INFO 93762 --- [foodduck] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
> Task :test FAILED
TodoServiceTest > 할 일 저장 시 User 반환, 날씨 정보, todo 새애성, db 저장을 정상적으로 수행해야 합니다. FAILED
org.opentest4j.AssertionFailedError at TodoServiceTest.java:68
1 test completed, 1 failed
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/mun/Desktop/2025/spring-plus/build/reports/tests/test/index.html
* Try:
> Run with --scan to get full insights.
BUILD FAILED in 8s
4 actionable tasks: 1 executed, 3 up-to-date
허허..
축약하자면,
expected: UserResponse@xxxx but was: UserResponse@yyyy
테스트 실행 중 UserResponse 객체를 비교하는 부분에서 객체 주소값이 다르다는 AssertionFailedError가 발생.
expected: org.example.expert.domain.user.dto.response.UserResponse@4ce6f39e
but was: org.example.expert.domain.user.dto.response.UserResponse@266e0341
👉 두 객체가 동일하지 않다는 오류.
즉, UserResponse 객체가 서로 다르게 인식되고 있음.
package org.example.expert.domain.todo.service;
import org.example.expert.client.WeatherClient;
import org.example.expert.domain.common.dto.AuthUser;
import org.example.expert.domain.todo.dto.request.TodoSaveRequest;
import org.example.expert.domain.todo.dto.response.TodoSaveResponse;
import org.example.expert.domain.todo.entity.Todo;
import org.example.expert.domain.todo.repository.TodoRepository;
import org.example.expert.domain.user.dto.response.UserResponse;
import org.example.expert.domain.user.entity.User;
import org.example.expert.domain.user.enums.UserRole;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/*
현재 기준에서, @SpringBootTest + @DataJpaTest + Testcontainers
방식이 가장 많이 쓰이는 것 같아서 해당 방식으로 line 별로 테스트 코드 구현.
*/
@SpringBootTest
@Transactional
public class TodoServiceTest {
private TodoService todoService;
@MockitoBean
private TodoRepository todoRepository;
@MockitoBean
private WeatherClient weatherClient;
@BeforeEach
void setUp() {
todoService = new TodoService(todoRepository, weatherClient);
}
@Test
@DisplayName("할 일 저장 시 User 반환, 날씨 정보, todo 생성, db 저장을 정상적으로 수행해야 합니다.")
void saveTodo_성공() {
// given
AuthUser authUser = new AuthUser(1L, "test@user.com", UserRole.USER);
TodoSaveRequest request = new TodoSaveRequest("할 일 title", "할 일 내용");
String expectedWeather = "맑음";
// Mock 설정
when(weatherClient.getTodayWeather()).thenReturn(expectedWeather);
when(todoRepository.save(any(Todo.class))).thenAnswer(invocation -> {
Todo todo = invocation.getArgument(0);
//ID 포함된 todo 객체 반환
Todo savedTodo = new Todo(todo.getTitle(), todo.getContents(), todo.getWeather(), todo.getUser());
return setTodoId(savedTodo, 1L);
});
// when
TodoSaveResponse response = todoService.saveTodo(authUser, request);
// then
User expectedUser = User.fromAuthUser(authUser);
//필드 값만 비교 (객체 주소 무시)
assertThat(response.getUser())
.usingRecursiveComparison()
.isEqualTo(new UserResponse(expectedUser.getId(), expectedUser.getEmail()));
// 날씨 정보 가져오는 거 검증
verify(weatherClient, times(1)).getTodayWeather();
// 투두 생성 확인
ArgumentCaptor<Todo> todoCaptor = ArgumentCaptor.forClass(Todo.class);
verify(todoRepository, times(1)).save(todoCaptor.capture());
Todo savedTodo = todoCaptor.getValue();
assertThat(savedTodo.getTitle()).isEqualTo(request.getTitle());
assertThat(savedTodo.getContents()).isEqualTo(request.getContents());
assertThat(savedTodo.getWeather()).isEqualTo(expectedWeather);
assertThat(savedTodo.getUser())
.usingRecursiveComparison()
.isEqualTo(expectedUser);
// 응답 객체 검증
assertThat(response.getId()).isNotNull();
assertThat(response.getTitle()).isEqualTo(request.getTitle());
assertThat(response.getContents()).isEqualTo(request.getContents());
assertThat(response.getWeather()).isEqualTo(expectedWeather);
}
// 리플렉션을 활용한 ID 강제 설정
private Todo setTodoId(Todo todo, Long id) {
try {
java.lang.reflect.Field idField = Todo.class.getDeclaredField("id");
idField.setAccessible(true);
idField.set(todo, id);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("ID 설정 실패", e);
}
return todo;
}
}
최종 수정
package org.example.expert.domain.todo.service;
import org.example.expert.client.WeatherClient;
import org.example.expert.domain.common.dto.AuthUser;
import org.example.expert.domain.todo.dto.request.TodoSaveRequest;
import org.example.expert.domain.todo.dto.response.TodoSaveResponse;
import org.example.expert.domain.todo.entity.Todo;
import org.example.expert.domain.todo.repository.TodoRepository;
import org.example.expert.domain.user.dto.response.UserResponse;
import org.example.expert.domain.user.entity.User;
import org.example.expert.domain.user.enums.UserRole;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/*
현재 기준에서, @SpringBootTest + @DataJpaTest + Testcontainers
방식이 가장 많이 쓰이는 것 같아서 해당 방식으로 line 별로 테스트 코드 구현.
*/
@SpringBootTest
@Transactional
public class TodoServiceTest {
private TodoService todoService;
@MockitoBean
private TodoRepository todoRepository;
@MockitoBean
private WeatherClient weatherClient;
@BeforeEach
void setUp() {
todoService = new TodoService(todoRepository, weatherClient);
}
@Test
@DisplayName("할 일 저장 시 User 반환, 날씨 정보, todo 생성, db 저장을 정상적으로 수행해야 합니다.")
void saveTodo_성공() {
// given
AuthUser authUser = new AuthUser(1L, "test@user.com", UserRole.USER);
TodoSaveRequest request = new TodoSaveRequest("할 일 title", "할 일 내용");
String expectedWeather = "맑음";
// Mock 설정
when(weatherClient.getTodayWeather()).thenReturn(expectedWeather);
when(todoRepository.save(any(Todo.class))).thenAnswer(invocation -> {
Todo todo = invocation.getArgument(0);
//ID 포함된 todo 객체 반환
Todo savedTodo = new Todo(todo.getTitle(), todo.getContents(), todo.getWeather(), todo.getUser());
return setTodoId(savedTodo, 1L);
});
// when
TodoSaveResponse response = todoService.saveTodo(authUser, request);
// then
User expectedUser = User.fromAuthUser(authUser);
//필드 값만 비교 (객체 주소 무시)
assertThat(response.getUser())
.usingRecursiveComparison()
.isEqualTo(new UserResponse(expectedUser.getId(), expectedUser.getEmail()));
// 날씨 정보 가져오는 거 검증
verify(weatherClient, times(1)).getTodayWeather();
// 투두 생성 확인
ArgumentCaptor<Todo> todoCaptor = ArgumentCaptor.forClass(Todo.class);
verify(todoRepository, times(1)).save(todoCaptor.capture());
Todo savedTodo = todoCaptor.getValue();
assertThat(savedTodo.getTitle()).isEqualTo(request.getTitle());
assertThat(savedTodo.getContents()).isEqualTo(request.getContents());
assertThat(savedTodo.getWeather()).isEqualTo(expectedWeather);
assertThat(savedTodo.getUser())
.usingRecursiveComparison()
.isEqualTo(expectedUser);
// 응답 객체 검증
assertThat(response.getId()).isNotNull();
assertThat(response.getTitle()).isEqualTo(request.getTitle());
assertThat(response.getContents()).isEqualTo(request.getContents());
assertThat(response.getWeather()).isEqualTo(expectedWeather);
}
// 리플렉션을 활용한 ID 강제 설정
private Todo setTodoId(Todo todo, Long id) {
try {
java.lang.reflect.Field idField = Todo.class.getDeclaredField("id");
idField.setAccessible(true);
idField.set(todo, id);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("ID 설정 실패", e);
}
return todo;
}
}
Lazy Loading
@Entity
public class Todo {
@Id @GeneratedValue
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY) // Lazy 로딩 설정
@JoinColumn(name = "user_id")
private User user;
}
Todo todo = todoRepository.findById(1L).orElseThrow();
User user = todo.getUser(); //여기까지는 실제 DB 조회 안 됨 (Proxy 객체 반환)
System.out.println(user.getEmail()); //이 시점에서 쿼리 실행됨!
Jpa가 Lazy Loading을 구현하는 방법
Todo todo = todoRepository.findById(1L).orElseThrow();
User user = todo.getUser();
System.out.println(user.getClass());
// 실제 User가 아니라 User$$HibernateProxy (프록시 객체 반환)
프록시 객체를 초기화하는 방법 ?
페치 조인(Fetch Join)
예제 (N+1 문제 발생)
public List<Todo> findAllTodos() {
return todoRepository.findAll();
}
// select * from todos; (여기까지는 1번)
// 이후 각 Todo마다 Lazy로딩된 User 조회 → N번 추가 쿼리 발생!
해결 방법(fetch join 사용)
@Query("SELECT t FROM Todo t JOIN FETCH t.user")
List<Todo> findAllTodosWithUsers();
정리
최적의 선택은 ?
public String createToken(Long userId, String email, UserRole userRole, String nickname) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("email", email)
.claim("userRole", userRole)
.claim("nickname",nickname)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact();
}
public String getNicknameFromToken(String token) {
Claims claims = extractClaims(token);
return claims.get("nickname", String.class);
}
jwt에서 nickname을 추출해서 요청 속성에 추가해야합니다.
즉, jwt에서 nickname을 추출해야 하고, DoFilter()메서드에서 요청 속성(request.setAttribuyte())에 닉넴 추가. 그 후, 닉네임이 필요할 때 요청 속성으로 가져올 수 있도록 설정만 해주면 됩니다.
프론트엔드에서 request.getAttribute("nickname")으로 닉네임 접근 가능하도록 수정했으며, 닉넴을 활용할 컨트롤러에서도 요청 속성으로 쉽게 사용 가능합니다.
request 먼저 수정해주고,
회원가입 후 jwt토큰을 반환하는 구조를 갖고 있습니다.
여기에 추가해주면됩니다.
아니면 @AllargsConstructor어노테이션을 추가해도 됩니다.
이제 프론트엔드에서 닉네임을 활용 가능합니다.
1) SignupRequest에서 nickname 값을 받아와서 저장해야함.
2) User 엔티티를 저장할 때, nickname을 포함해야함.
3) jwt 생성 시 nickname을 추가해야함.
4) SignupResponse에도 nickname을 포함해야함.
회원가입 Service먼저 수정
package org.example.expert.domain.auth.service;
import lombok.RequiredArgsConstructor;
import org.example.expert.config.JwtUtil;
import org.example.expert.config.PasswordEncoder;
import org.example.expert.domain.auth.dto.request.SigninRequest;
import org.example.expert.domain.auth.dto.request.SignupRequest;
import org.example.expert.domain.auth.dto.response.SigninResponse;
import org.example.expert.domain.auth.dto.response.SignupResponse;
import org.example.expert.domain.auth.exception.AuthException;
import org.example.expert.domain.common.exception.InvalidRequestException;
import org.example.expert.domain.user.entity.User;
import org.example.expert.domain.user.enums.UserRole;
import org.example.expert.domain.user.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
@Transactional
public SignupResponse signup(SignupRequest signupRequest) {
if (userRepository.existsByEmail(signupRequest.getEmail())) {
throw new InvalidRequestException("이미 존재하는 이메일입니다.");
}
String encodedPassword = passwordEncoder.encode(signupRequest.getPassword());
UserRole userRole = UserRole.of(signupRequest.getUserRole());
User newUser = new User(
signupRequest.getEmail(),
encodedPassword,
signupRequest.getNickname(),
userRole
);
User savedUser = userRepository.save(newUser);
String bearerToken = jwtUtil.createToken(savedUser.getId(),
savedUser.getEmail(),
savedUser.getNickname(),
userRole);
return new SignupResponse(bearerToken, savedUser.getNickname());
}
public SigninResponse signin(SigninRequest signinRequest) {
User user = userRepository.findByEmail(signinRequest.getEmail()).orElseThrow(
() -> new InvalidRequestException("가입되지 않은 유저입니다."));
// 로그인 시 이메일과 비밀번호가 일치하지 않을 경우 401을 반환합니다.
if (!passwordEncoder.matches(signinRequest.getPassword(), user.getPassword())) {
throw new AuthException("잘못된 비밀번호입니다.");
}
String bearerToken = jwtUtil.createToken(user.getId(),
user.getEmail(),
user.getNickname(),
user.getUserRole());
return new SigninResponse(bearerToken, user.getNickname());
}
}
AuthService 수정하다보면 빨간 표시가 많이 뜸. ->
수정하지 않은게 많아서 그럼. 필드를 추가했기 때문에, 생성자도 당연히 추가해줘야함. 일단 SigninResponse랑 SigninRequest 수정해줌.
매개변수 순서 수정.
yo, nice man